File:  [Isaki's NoNo m68k/m88k emulator] / nono / vm / console.cpp
Revision 1.1.1.6 (vendor branch): download - view: text, annotated - select for diffs
Wed Apr 29 17:05:59 2026 UTC (2 months, 3 weeks ago) by root
Branches: MAIN, Isaki
CVS tags: v027, HEAD
nono 1.7.0

//
// nono
// Copyright (C) 2024 nono project
// Licensed under nono-license.txt
//

//
// メイン画面を vt220 互換コンソールにする
//

// https://vt100.net/docs/vt220-rm/contents.html

// vttest ver2.7 (20210210) result
//
// OK	1. (1)	枠表示、DECALN、80桁
// OK	1. (2)	枠表示、DECALN、132桁
// OK	1. (3)	Test of autowrap、80桁
// OK	1. (4)	Test of autowrap、132桁
// OK	1. (5)	Test of cursor-control
// OK	1. (6)	Test of leading zeros in ESC
// FAIL	2. (1)	Test of WRAP AROUND
// FAIL	2. (2)	Test of TAB setting/resetting
// OK*	2. (3)	132 column, light background (*背景色の区別なし)
// OK*	2. (4)	80  column, light background (*背景色の区別なし)
// OK*	2. (5)	132 column, dark background  (*背景色の区別なし)
// OK*	2. (6)	80  column, dark background  (*背景色の区別なし)
// OK*	2. (7)	Soft scroll down size 2  (*Soft/Jump の区別なし)
// OK*	2. (8)	Soft scroll down size 24 (*Soft/Jump の区別なし)
// OK*	2. (9)	Jump scroll down size 2  (*Soft/Jump の区別なし)
// OK*	2. (10)	Jump scroll down size 24 (*Soft/Jump の区別なし)
// OK	2. (11)	Origin mode test. Bottom
// OK	2. (12)	Origin mode test. Top
// OK*	2. (13) Graphic rendition test. Dark  (*背景色の区別なし)
// OK*	2. (14) Graphic rendition test. Light (*背景色の区別なし)
// OK	2. (15) Test of SAVE/RESTORE
// OK*	3.		character sets (DEC 罫線以外対応予定なし)
// FAIL	4.		double-sized characters は対応予定なし
// -	5.1		LED Lights
// FAIL	5.2		Auto Repeat
// -	5.3		KeyBoard Layout
// OK	5.4		Cursor Keys (VT52 モードのテストは動かない)
// -	5.5		Numeric Keypad
// -	5.6		Editing Keypad (?)
// -	5.7		Function Keys (?)
// -	5.8		AnswerBack (?)
// FAIL	5.9		Control Keys (CTRL-@ が通らない)
// -	6.1		<ENQ> (?)
// FAIL	6.2		Set/Reset Mode
// OK	6.3		Device Status Report (DSR)
// OK	6.4		Primary Device Attributes (DA)
// OK	6.5		Secondary Device Attributes (DA)
// -	6.6		Tertiary Device Attributes (DA) (*VT420以上)
// -	6.7		Request Terminal Parameters (DECREQTPARM) (*VT100)
// -	7.		VT52 mode
// OK	8. (1)	Screen accordion test
// OK	8. (2)	Top A, Bottom X
// OK	8. (3)	Test of Insert mode, A**B
// OK	8. (4)	Test of Delete Char, AB
// OK	8. (5)	The right column should be staggered
// FAIL	8. (6)	〃 (double-sized)
// OK	8. (7)	ANSI Insert Char
// OK	8. (8)	Screen Accordion test (以下132桁)
// OK	8. (9)	Top A, Bottom X
// OK	8. (10)	Test of Insert mode, A**B
// OK	8. (11) Test of Delete char, AB
// OK	8. (12)	The right column should be staggered
// FAIL	8. (13)	〃 (double-sized)
// OK	8. (14)	ANSI Insert Char
// -	9.		Test of known bugs
// OK?	10.1	RIS
// FAIL	10.2	DECTST
// OK?	10.3	DECSTR
// OK	11.1.1.1.1		Keyboard Status
// OK	11.1.1.1.2		Operating Status
// OK	11.1.1.1.3		Printer Status
// OK	11.1.1.1.4		UDK Status
// ?	11.1.2.1		SRM
// OK	11.1.2.2		DECTCEM
// OK	11.1.2.3		ECH
// FAIL	11.1.2.4 (1)	DECSCA, DECSED, DECSEL
// OK	11.1.2.4 (2)	DECSCA, ignoring EL/ED
// OK	11.1.2.4 (3)	DECSCA, ignoring ECH/ICH/DCH
// FAIL	11.1.3			S7C1T/S8C1T
// FAIL	11.1.4			Printer(MC)
// FAIL	11.1.5			DECDLD
// OK	11.1.6			DECSTR
// FAIL	11.1.7			DECUDK
// -	11.2	VT320 features
// -	11.3	VT420 features
// -	11.4	VT520 features
// -	11.5	ISO-6429 cursor-movement
// -	11.6	ISO-6429 colors
// -	11.7	Other ISO-6429 features
// -	11.8	XTERM special features

#include "console.h"
#include "bitmap.h"
#include "builtinrom.h"
#include "event.h"
#include "comdriver_cons.h"
#include "mainapp.h"
#include "scheduler.h"
#include "syncer.h"
#include <algorithm>

// エスケープシーケンス等。
#define ESC		'\x1b'

#define PRINTLOG(fmt...)	do {	\
	if (log) {	\
		fprintf(log, fmt);	\
		fflush(log);	\
	}	\
} while (0)

// コンストラクタ
ConsoleDevice::ConsoleDevice()
	: inherited(OBJ_CONSOLE)
{
	screen.resize(fullwidth * height);
	SetPalette(false);
}

// デストラクタ
ConsoleDevice::~ConsoleDevice()
{
	if (log) {
		fclose(log);
		log = NULL;
	}

	if (comdriver) {
		comdriver->DetachConsole();
		comdriver = NULL;
	}
}

// 初期化
bool
ConsoleDevice::Init()
{
	renderer = GetVideoRenderer();
	keyboard = gMainApp.GetObject<ConsoleKeyboard>(OBJ_KEYBOARD);
	syncer = GetSyncer();

	// ログファイル。
	if (gMainApp.console_logfile) {
		log = fopen(gMainApp.console_logfile, "w");
		if (log == NULL) {
			warn("console logfile \"%s\"", gMainApp.console_logfile);
			return false;
		}
	}

	auto evman = GetEventManager();
	vsync_event = evman->Regist(this,
		ToEventCallback(&ConsoleDevice::VSyncCallback),
		"Console V-Sync");
	vsync_event->time = 16666.667_usec;

	return true;
}

// COM ドライバを接続。
// (HostCOM の cons ドライバが呼ぶ)
void
ConsoleDevice::Attach(COMDriverConsole *comdriver_)
{
	comdriver = comdriver_;

	SetPalette((comdriver != NULL));
}

void
ConsoleDevice::ResetHard(bool poweron)
{
	if (poweron) {
		// 最初に一回フチを含めて描画する。
		init_screen = true;

		// シリアルコンソールなのでリセットには反応しない。
		Clear();
		Locate(0, 0);
	}

	scheduler->RestartEvent(vsync_event);

	ResetTerminalHard();
}

void
ConsoleDevice::VSyncCallback(Event *ev)
{
	bool update_needed;

	// 更新のあった行については、点滅があるか全て調べ直す。
	for (int y = 0; y < dirty.size(); y++) {
		if (__predict_false(dirty[y])) {
			has_blink.set(y, std::any_of(
				screen.begin() + y * fullwidth,
				screen.begin() + y * fullwidth + fullwidth,
				[](int v) { return (v & ATTR_BLINK); }));
		}
	}

	// 点滅を持つ行は周期ごとに dirty に追加。
	if (__predict_false(has_blink.any())) {
		auto rtime = syncer->GetRealTime();
		// nsec を >>29 で 536_msec くらいになる
		auto new_blinking = (bool)(((rtime/*_nsec*/) >> 29) & 1);
		if (blinking != new_blinking) {
			blinking = new_blinking;
			dirty |= has_blink;
		}
	} else {
		// 点滅が一つもなくなったら点滅状態を解除。
		blinking = false;
	}

	// 更新 (dirty[]) があれば pending[] に重ねる。
	{
		std::lock_guard<std::mutex> lock(mtx);
		pending |= dirty;
		update_needed = pending.any();
	}

	dirty.reset();

	// 更新の必要があれば Render に作画指示。
	if (update_needed) {
		renderer->NotifyRender();
	}

	scheduler->StartEvent(ev);
}

// パレットの状態を変更する。
void
ConsoleDevice::SetPalette(bool console_is_active)
{
	if (console_is_active) {
		palette[0] = UD_LIGHT_GREY;	// BG
		palette[1] = UD_BLACK;		// FG
	} else {
		palette[0] = UD_LIGHT_GREY;	// BG
		palette[1] = UD_GREY;		// FG
	}
	// 反転色。
	palette[2] = palette[0];

	// 画面に即反映させる。
	{
		std::lock_guard<std::mutex> lock(mtx);
		pending.set();
	}
}

// ソフトリセット (DECSTR 用)。
void
ConsoleDevice::ResetTerminalSoft()
{
	cursor_visible = true;
	insert_mode = false;
	decom = false;
	keyboard->SetCKM(false);
	ResetScroll();

	save_x = 0;
	save_y = 0;
	save_attr = 0;
	save_wrap = false;
	save_decom = false;
}

// 端末の状態をハードリセットする。
// (画面のクリアはここではしない?)
void
ConsoleDevice::ResetTerminalHard()
{
	ResetTerminalSoft();

	ResetState();
	SetAttr(0);

	wrap_pending = false;
	width = 80;
	ResetMargin();

	cur_x = 0;
	cur_y = 0;
	cr_on_lf = false;
}

// 左右マージンをリセットする。
void
ConsoleDevice::ResetMargin()
{
	// margin は両閉区間で表す。
	margin_left = 0;
	margin_right = width - 1;
}

// スクロール領域をリセットする。
void
ConsoleDevice::ResetScroll()
{
	// scroll は両閉区間で表す。
	scroll_top = 0;
	scroll_btm = height - 1;
}

// エスケープシーケンスの状態を初期化する。
void
ConsoleDevice::ResetState()
{
	state = 0;
	seq.clear();
	arg.clear();
	argv.clear();
	argv_idx = -1;
}

// ch をログ出力する。
void
ConsoleDevice::PutcharDebug(uint32 ch)
{
	if (ch == '\n') {
		// 改行が来たらログも改行する。
		fputs("\\n\n", log);
		log_newline = true;
	} else {
		if (ch == ESC) {
			// 読みやすさのため ESC の前で改行したい。
			if (log_newline == false) {
				fputc('\n', log);
			}
			fputs("ESC", log);
		} else if (ch == '\r') {
			fputs("\\r", log);
		} else if (ch == '\t') {
			fputs("\\t", log);
		} else if (ch < 0x20 || ch >= 0x7f) {
			fprintf(log, "\\x%02x", ch);
		} else {
			fputc(ch, log);
		}
		log_newline = false;
	}
	fflush(log);
}

// シリアルコンソールから来る出力。
// ホストドライバスレッドから呼ばれる。
void
ConsoleDevice::Putchar(uint32 ch)
{
	if (__predict_false(log)) {
		PutcharDebug(ch);
	}

	// 処理したら return。処理できなかったら switch を抜ける。
	switch (state) {
	 case ESC:
		if (PutcharESC(ch)) {
			return;
		}
		break;

	 case '[':	// CSI
		if (PutcharCSI(ch)) {
			return;
		}
		break;

	 case '(':	// G0
		if (PutcharG0(ch)) {
			return;
		}
		break;

	 case '?':	// CSI '?'
		if (PutcharCSIQ(ch)) {
			return;
		}
		break;

	 case '#':
		if (PutcharSharp(ch)) {
			return;
		}
		break;

	 case ')':	// G1
	 case '*':	// G2
	 case '+':	// G3
		// とりあえず何もしない。
		ResetState();
		return;

	 default:
		PutcharPlain(ch);
		return;
	}

	// シーケンスを破棄して平文に戻る。
	putlog(0, "Unknown Sequence:%s", Dump(ch).c_str());
	ResetState();
}

// 何のシーケンスでもない1文字を表示する。
void
ConsoleDevice::PutcharPlain(uint32 ch)
{
	switch (ch) {
	 case '\x00':	// NUL
		// 何もしない。パディングというか NOP 代わりに大量に送られてくる。
		return;

	 case '\x01':	// SOH
	 case '\x02':	// STX
	 case '\x03':	// ETX
	 case '\x04':	// EOT
	 case '\x05':	// ENQ
	 case '\x06':	// ACK
		// 何もしない。
		return;

	 case '\x07':	// BEL
		// とりあえず無視。
		putlog(2, "BEL (Ignored)");
		return;

	 case '\x08':	// BS
		// カーソルを 1 文字左に移動。文字は消さない。
		// DECLRMM(左右マージン)の影響を受ける。
		LocateXinMargin(cur_x - 1);
		return;

	 case '\t':		// HT
		HT();
		return;

	 case '\n':		// LF
	 case '\x0b':	// VT
	 case '\x0c':	// FF
		LF();
		return;

	 case '\r':		// CR
		CR();
		return;

	 case '\x0e':	// SO
	 case '\x0f':	// SI
	 case '\x10':	// DLE
	 case '\x11':	// DC1/XON
	 case '\x12':	// DC2
	 case '\x13':	// DC3/XOFF
	 case '\x14':	// DC4
	 case '\x15':	// NAK
	 case '\x16':	// SYN
	 case '\x17':	// ETB
		// 何もしない。
		return;

	 case '\x18':	// CAN
		// Cancel
		ResetState();
		return;

	 case '\x19':	// EM
		// 何もしない。
		return;

	 case '\x1a':	// SUB
		ResetState();
		return;

	 case ESC:		// ESC
		seq.push_back(ch);
		state = ch;
		return;

	 case '\x1c':	// FS (File Separator)
	 case '\x1d':	// GS (Group Separator)
	 case '\x1e':	// RS (Record Separator)
	 case '\x1f':	// US (Unit Separator)
		// 何もしない。
		return;

	 case '\x7f':	// DEL
		// 何もしない。
		return;

	 default:
		// ここが通常の1文字出力。
		// 8ビット文字はここでは素通しするが、レンダラで豆腐を出力する。
		// NetBSD の sysinst が言語選択画面で1文字送ってくる。
		// 8ビットのエスケープシーケンス (\x80〜\x9f) のことは無視する。
		PutcharNormal(ch);
		return;
	}
}

// ESC の次の文字を処理する。
// 処理すれば true、知らないシーケンスなので中止する場合は false を返す。
bool
ConsoleDevice::PutcharESC(uint32 ch)
{
	switch (ch) {
	 case '[':		// CSI
	 case '(':		// G0
	 case ')':		// G1
	 case '*':		// G2
	 case '+':		// G3
	 case '#':		// Font width/height control
		seq.push_back(ch);
		state = ch;
		return true;

	 case '\\':		// ST
		putlog(2, "ST");
		break;

	 case ' ':		// Select C1 Controls
		// ESC ' ' F (S7C1T)
		// ESC ' ' G (S8C1T)
		return Unimpl(ch, "S*C1T");

	 // DECKPAM/DECKPNM はテンキー入力をそのまま送る(Keypad Numeric Mode)か、
	 // Keypad Application Mode で送るかの切り替えだが、今の実装では
	 // 文字コードしかここに来ていないためテンキーかどうかはもう区別できない。
	 // ので、テンキーが存在していないか、あってもあえて押していないのと等価。
	 // vi(1) の開始/終了時に送られてくる。
	 case '=':		// DECKPAM
		putlog(3, "DECKPAM (Keypad App. Mode)");
		break;
	 case '>':		// DECKPNM
		putlog(3, "DECKPNM (Keypad Num. Mode)");
		break;

	 case '7':		// DECSC (カーソル位置保存)
		putlog(2, "DECSC (Save)");
		save_x = cur_x;
		save_y = cur_y;
		save_attr = cur_attr;
		save_wrap = wrap_pending;
		save_decom = decom;
		break;

	 case '8':		// DECRC (カーソル位置復帰)
		// 変数の復帰順序注意。
		putlog(2, "DECRC (Restore)");
		wrap_pending = save_wrap;
		decom = save_decom;
		cur_attr = save_attr;
		Locate(save_x, save_y);
		break;

	 case 'c':		// RIS (Hard terminal reset)
		putlog(2, "RIS (Hard terminal reset)");
		ResetTerminalHard();
		Clear();
		break;

	 case 'D':		// IND
	 {
		// カーソルを1行下に移動する。
		// ただしカーソルがスクロール下端行にいる場合はスクロール領域全体を
		// 1行上に移して、カーソル行を空行にする。
		// DECLRMM(左右マージン)の影響は受けない。
		//
		// スクロール領域外でも1行下に移動するだけなので、スクロール上端か
		// どうかだけ。
		wrap_pending = false;
		if (cur_y != scroll_btm) {
			putlog(3, "IND (Index) ");
			LocateY(cur_y + 1);
		} else {
			// 下端から一つ下に移動しようとした。
			putlog(3, "IND (Index)  scrolled");
			for (int y = scroll_top + 1; y <= scroll_btm; y++) {
				CopyLineWhole(y - 1, y);
			}
			EraseLineWhole(scroll_btm);
		}
		break;
	 }

	 case 'E':		// NEL (Next Line)
		// カーソルを次行の先頭に。スクロール領域下端に来たらスクロールアップ。
		// なので CR+LF と等価。
		putlog(3, "NEL (Next Line)");
		CR();
		LF();
		break;

	 case 'H':		// HTS (Horizontal tab set)
		return Unimpl(ch, "HTS");

	 case 'M':		// RI (Reverse Index)
	 {
		// カーソルを1行上に移動する。
		// ただしカーソルがスクロール上端行にいる場合はスクロール領域全体を
		// 1行下に移して、カーソル行を空行にする。
		// DECLRMM(左右マージン)の影響は受けない。
		//
		// スクロール領域外でも1行上に移動するだけなので、スクロール上端か
		// どうかだけ。
		wrap_pending = false;
		if (cur_y != scroll_top) {
			putlog(3, "RI (Rev. Index)");
			LocateY(cur_y - 1);
		} else {
			// 上端から一つ上に移動しようとした。
			putlog(3, "RI (Rev. Index) scrolled");
			for (int y = scroll_btm - 1; y >= scroll_top; y--) {
				CopyLineWhole(y + 1, y);
			}
			EraseLineWhole(scroll_top);
		}
		break;
	 }

	 case 'N':		// SS2
		return Unimpl(ch, "SS2");
	 case 'O':		// SS3
		return Unimpl(ch, "SS3");

	 case 'Z':		// DECID (Identify Terminal)
		// VT100 か問い合わせなので、常に VT100 相当を返答。
		putlog(3, "DECID (Identify Terminal)");
		SendString("\x1b/Z");
		break;

	 case '$':		// 94 multibyte (VT300 シリーズ以降?)
	 default:
		return false;
	}

	ResetState();
	return true;
}

// CSI の次の文字を処理する。
// 処理すれば true、知らないシーケンスなので中止する場合は false を返す。
bool
ConsoleDevice::PutcharCSI(uint32 ch)
{
	switch (ch) {
	 case '\x00'
	  ... '\x17':
	 case '\x19':	// EM
	 case '\x1c':	// FS (File Separator)
	 case '\x1d':	// GS (Group Separator)
	 case '\x1e':	// RS (Record Separator)
	 case '\x1f':	// US (Unit Separator)
	 case '\x7f':	// DEL
		// 制御文字はその場で実行される。
		PutcharPlain(ch);
		// ステート完了ではないので break ではなく return。
		return true;

	 case '\x18':	// CAN
		// Cancel
		putlog(3, "CAN");
		break;
	 case '\x1a':	// SUB
		putlog(3, "SUB");
		break;

	 case ESC:		// ESC
		// ここまでのを捨てて新しく始める?らしい。
		ResetState();
		PutcharPlain(ch);
		return true;

	 case '0':
	 case '1':
	 case '2':
	 case '3':
	 case '4':
	 case '5':
	 case '6':
	 case '7':
	 case '8':
	 case '9':
	 case ';':
	 case '\"':
	 case '$':
	 case '>':
	 case '!':
		seq.push_back(ch);
		arg.push_back(ch);
		return true;

	 case '@':		// ICH (Insert Character)
	 {
		// カーソル位置に n 個空白を挿入。
		// カーソル位置とそれ以降の文字は右にずれる。
		// カーソル位置は変わらない。
		// DECLRMM(左右マージン)の影響を受ける。
		int n = GetArgDefault(1);
		putlog(3, "ICH (Insert Char) %u", n);
		int maxlen = margin_right - cur_x + 1;
		n = std::min(n, maxlen);
		memmove(&screen[cur_y * fullwidth + (cur_x + n)],
			&screen[cur_y * fullwidth + cur_x],
			(maxlen - n) * sizeof(screen[0]));
		for (int i = 0; i < n; i++) {
			Setchar(cur_x + i, cur_y, ' ' | cur_attr);
		}
		dirty.set(cur_y);
		break;
	 }

	 case 'A':		// CUU
	 {
		// カーソルを n 行上に移動。
		// DECSTBM(スクロール領域)の影響を受ける。
		int n = GetArgDefault(1);
		putlog(3, "CUU (Cursor Up) %u", n);
		LocateYinScroll(cur_y - n);
		break;
	 }

	 case 'B':		// CUD
	 {
		// カーソルを下に移動。
		// DECSTBM(スクロール領域)の影響を受ける。
		int n = GetArgDefault(1);
		putlog(3, "CUD (Cursor Down) %u", n);
		LocateYinScroll(cur_y + n);
		break;
	 }

	 case 'C':		// CUF
	 {
		// カーソルを右に移動。
		// DECLRMM(左右マージン)の影響を受ける。
		int n = GetArgDefault(1);
		putlog(3, "CUF (Cursor Forward) %u", n);
		LocateXinMargin(cur_x + n);
		break;
	 }

	 case 'D':		// CUB
	 {
		// カーソルを左に移動。
		// DECLRMM(左右マージン)の影響を受ける。
		int n = GetArgDefault(1);
		putlog(3, "CUF (Cursor Backward) %u", n);
		LocateXinMargin(cur_x - n);
		break;
	 }

	 case 'c':		// DA
	 {
		// デバイス属性を応答する。
		// CSI [0] c なら Primary DA (DA1)。
		// CSI > c なら Secondary DA (DA2)。
		int argc = ParseArg();
		if (argc > 0 && argv[0][0] == '>') {
			// DA2はファームウェアバージョンとハードウェアオプション。
			// どちらも関係ないので VT220('1'), ver 1.0 ('10'),
			// no options ('0') とだけ返す。
			putlog(3, "DA2 (Device Attributes)");
			SendString("\x1b[>1;10;0c");
		} else {
			// とりあえず他は全部 DA1 ということにしておく。
			// DA1 に対する応答は今のところ 62 (VT220) のみ。
			putlog(3, "DA (Device Attribute)");
			SendString("\x1b[?62c");
		}
		break;
	 }

	 case 'f':		// HVP
		// HVP は VT52 互換性のため存在しており CUP と動作は同じらしい。
	 case 'H':		// CUP
	 {
		// CSI <y1> ; <x1> H … (x1, y1) にカーソルを移動。
		// DECOM=0 なら画面左上が原点。
		// DECOM=1 なら DECSTBM/DECLRMM の左上を原点とする。
		int y1 = GetArgDefault(1);
		int x1 = GetArgDefault(1);
		putlog(3, "CUP (Cursor Position) row=%u col=%u", y1, x1);
		int x = x1 - 1;
		int y = y1 - 1;
		if (decom) {
			Locate(margin_left + x, scroll_top + y);
		} else {
			Locate(x, y);
		}
		break;
	 }

	 case 'J':		// ED (Erase Display)
	 {
		// CSI [<n>] J
		// <n> が 0 か省略なら、カーソル位置から画面右下までを消去。
		// <n> が 1 なら、カーソル位置から左上までを消去。
		// <n> が 2 なら、全画面消去。
		// DECLRMM(左右マージン)/DECSTBM(スクロール領域)の影響は受けない。
		// wrap は解除される。文字の属性も消去される。
		// XXX SPA と ERM は未対応。
		wrap_pending = false;
		int pos = cur_y * fullwidth + cur_x;
		int n = GetArg();
		switch (n) {
		 default:	// 未定義値は 0 とする実装が多いらしい。知らんけど
		 case 0:
			putlog(3, "ED (Erase Display) Forward");
			std::fill(screen.begin() + pos, screen.end(), ' ');
			for (int y = cur_y; y < height; y++) {
				dirty.set(y);
			}
			break;
		 case 1:
			putlog(3, "ED (Erase Display) Backward");
			std::fill(screen.begin(), screen.begin() + pos + 1, ' ');
			for (int y = 0; y <= cur_y; y++) {
				dirty.set(y);
			}
			break;
		 case 2:
			putlog(3, "ED (Erase Display) All");
			std::fill(screen.begin(), screen.end(), ' ');
			dirty.set();
			break;
		}
		break;
	 }

	 case 'K':		// EL (Erase Line)
	 {
		// CSI [<n>] K
		// <n> が 0 か省略なら、カーソル位置の右側を消去。
		// <n> が 1 なら、カーソル位置の左側を消去。
		// <n> が 2 なら、カーソル行を消去。
		// DECLRMM (左右マージン) の影響は受けない。
		int n = GetArg();
		int x0;
		int x1;
		switch (n) {
		 default:	// 未定義値は 0 とする実装が多いらしい。知らんけど
		 case 0:
			putlog(3, "EL (Erase Line) Forward");
			x0 = cur_x;
			x1 = width - 1;
			break;
		 case 1:
			putlog(3, "EL (Erase Line) Backward");
			x0 = 0;
			x1 = cur_x;
			break;
		 case 2:
			putlog(3, "EL (Erase Line) All");
			x0 = 0;
			x1 = width - 1;
			break;
		}
		for (int x = x0; x <= x1; x++) {
			Setchar(x, cur_y, ' ' | cur_attr);
		}
		dirty.set(cur_y);
		break;
	 }

	 case 'L':		// IL (Insert Line)
	 {
		// CSI [<n>] L
		// カーソル行とそれより下を1行ずつ下に移動し、
		// カーソル行は空行にする。を n 回繰り返したもの。
		// DECLRMM (左右マージン) の影響は受けない。
		// DECSTBM (スクロール領域) の影響を受ける。
		int n = GetArgDefault(1);
		putlog(3, "IL (Insert Line) %u", n);

		// たぶん企図した時点で解除される?
		wrap_pending = false;

		// カーソルがスクロール領域外にいたら何もしない。
		if (cur_y < scroll_top || cur_y > scroll_btm) {
			break;
		}

		n = std::min(n, scroll_btm - cur_y + 1);
		int y;
		for (y = scroll_btm; y >= (cur_y + n); y--) {
			CopyLineWhole(y, y - n);
		}
		for (; y >= cur_y; y--) {
			EraseLineWhole(y);
		}
		// カーソルは先頭へ移動。
		LocateX(0);
		break;
	 }

	 case 'M':		// DL (Delete Line)
	 {
		// CSI [<n>] M
		// カーソル行を削除し、次行以降を1行ずつ上に移動して、
		// 最下行を空行にする。を n 回繰り返したもの。
		//
		// DECLRMM (左右マージン) の影響は受けない。
		// DECSTBM (スクロール領域) の影響を受ける。
		int n = GetArgDefault(1);
		putlog(3, "DL (Delete Line) %u", n);

		// たぶん企図した時点で解除される?
		wrap_pending = false;

		// カーソルがスクロール領域外にいたら何もしない。
		if (cur_y < scroll_top || cur_y > scroll_btm) {
			break;
		}

		n = std::min(n, scroll_btm + cur_y + 1);
		int y;
		for (y = cur_y; y < scroll_btm - n; y++) {
			CopyLineWhole(y, y + n);
		}
		for (; y < scroll_btm; y++) {
			EraseLineWhole(y);
		}
		// カーソルは先頭へ移動。
		LocateX(0);
		break;
	 }

	 case 'g':		// TBC (タブ設定をクリア)
		// CSI [<n>] g
		// <n> が 0 (か省略)なら、カーソル位置のタブをクリア。
		// <n> が 3 ならすべてのタブをクリア。
		return Unimpl(ch, "TBC");

	 case 'i':		// Printing
		return Unimpl(ch, "Printing");

	 case 'h':		// SM (オプションを設定)
	 case 'l':		// RM (オプションを解除)
	 {
		// CSI <n> h
		// CSI <n> l
		bool val = (ch == 'h');
		int argc = ParseArg();
		for (int i = 0; i < argc; i++) {
			int n = GetArg();
			switch (n) {
			 case 2:	// KAM (オンでキー入力を無視)
				return Unimpl(ch, "KAM");
			 case 4:	// IRM (Insert/Replace)
				if (val) {
					putlog(3, "Set IRM (to Insert)");
				} else {
					putlog(3, "Reset IRM (to Replace)");
				}
				insert_mode = val;
				break;
			 case 12:	// SRM (オンでローカルエコーを無効)
				return Unimpl(ch, "SRM");
			 case 20:	// LNM (オンで LF/VT/FF が行頭に移動)
				putlog(3, "%s LNM(New Line Mode)", val ? "Set" : "Reset");
				cr_on_lf = val;
				break;
			 default:
				putlog(2, "Ignore option %u %c", n, ch);
				break;
			}
		}
		(void)val;
		break;
	 }

	 case 'n':		// DSR (Device Status Report)
	 {
		int n = GetArg();
		switch (n) {
		 case 5:	// Request for terminal status
			// 障害が起きているかどうかの問い合わせのようだ。応答は
			// ESC [ 0 n … 障害なし
			// ESC [ 3 n … 障害あり
			putlog(3, "DSR 5(Request Terminal Status)");
			SendString("\x1b[0n");
			break;

		 case 6:	// Request for cursor position
		 {
			// カーソル位置の問い合わせ。(ANSI 仕様?)
			// DECOM (マージン原点) の影響を受ける。(vttest 6-3)
			putlog(3, "DSR 6(Request Cursor Position)");
			int x = cur_x;
			int y = cur_y;
			if (decom) {
				x -= margin_left;
				y -= scroll_top;
			}
			SendString(string_format("\x1b[%u;%uR", y + 1, x + 1));
			break;
		 }

		 default:
			return false;
		}
		break;
	 }

	 case 'm':		// SGR (Select Graphic Rendition)
	 {
		// CSI 7 ; 37 m みたいにセミコロンで区切ってパラメータを複数書ける。
		// パラメータには 38:2:rr:gg:bb みたいにコロンで区切ったひとかたまり
		// もある。
		int argc = ParseArg();
		if (argc == 0) {
			argc++;
		}
		if (loglevel >= 3) {
			std::string msg;
			for (int j = 0; j < argc; j++) {
				int n = GetArg();
				msg += string_format("; %u", n);
			}
			putlogn("SGR %s", msg.c_str() + 2);
			// XXX Peek がないがインデックスを戻せばまた使える。
			argv_idx = 0;
		}
		for (int i = 0; i < argc; i++) {
			int n = GetArg();
			switch (n) {
			 // VT220 で定義されているもの。
			 case 0:			// すべての属性を解除
				SetAttr(0);
				break;
			 case 1:			// ボールドをセット
				SetAttr(cur_attr | ATTR_BOLD);
				break;
			 case 4:			// 下線をセット
				SetAttr(cur_attr | ATTR_UNDERLINE);
				break;
			 case 5:			// 低速点滅をセット (対応予定なし)
				SetAttr(cur_attr | ATTR_BLINK);
				break;
			 case 7:			// 反転をセット
				SetAttr(cur_attr | ATTR_REVERSE);
				break;
			 case 22:			// ボールド(と低輝度)を解除
				SetAttr(cur_attr & ~ATTR_BOLD);
				break;
			 case 24:			// 下線を解除
				SetAttr(cur_attr & ~ATTR_UNDERLINE);
				break;
			 case 25:			// 点滅を解除
				SetAttr(cur_attr & ~ATTR_BLINK);
				break;
			 case 27:			// 反転を解除
				SetAttr(cur_attr & ~ATTR_REVERSE);
				break;

			 // それ以降で拡張されたものに勝手に対応してみる。

			 case 3:			// イタリックをセット
				SetAttr(cur_attr | ATTR_ITALIC);
				break;
			 case 9:			// 打ち消し線をセット
				SetAttr(cur_attr | ATTR_STRIKE);
				break;
			 case 23:			// イタリックを解除
				SetAttr(cur_attr & ~ATTR_ITALIC);
				break;
			 case 29:			// 打ち消し線を解除
				SetAttr(cur_attr & ~ATTR_STRIKE);
				break;
			 case 53:			// 上線をセット
				SetAttr(cur_attr | ATTR_OVERLINE);
				break;
			 case 55:			// 上線を取り消し
				SetAttr(cur_attr & ~ATTR_OVERLINE);
				break;

			 // 色も現状対応予定はないがメモも兼ねて並べておく。
			 case 30 ... 37:	// 文字色の設定
			 case 38:			// 文字色(拡張)を設定
			 case 39:			// 文字色をデフォルトに戻す
			 case 40 ... 47:	// 背景色を設定
			 case 48:			// 背景色(拡張)を設定
			 case 49:			// 背景色をデフォルトに戻す
			 case 90 ... 97:	// 文字色(高輝度)を設定
			 case 100 ... 107:	// 背景色(高輝度)を設定
			 default:
				// 無視
				break;
			}
		}
		break;
	 }

	 case 'p':		// DECSCL / DECSTR
	 {
		int argc = ParseArg();
		if (argc == 1 && argv[0] == "!") {
			// DECSTR
			putlog(2, "DECSTR (Soft Terminal Reset)");
			ResetTerminalSoft();
			break;
		} else {
			// DECSCL
			return Unimpl(ch, "DECSCL");
		}
	 }

	 case 'P':		// DCH (Delete Character)
	 {
		// カーソル位置から n 文字削除。
		// 左へずらして、右に空白を挿入。
		// カーソル位置は変わらない。
		int n = GetArgDefault(1);
		putlog(3, "DCH (Delete Char) %u", n);
		int maxlen = margin_right - cur_x + 1;
		n = std::min(n, maxlen);
		memmove(&screen[cur_y * fullwidth + cur_x],
			&screen[cur_y * fullwidth + (cur_x + n)],
			(maxlen - n) * sizeof(screen[0]));
		for (int i = 0; i < n; i++) {
			Setchar(margin_right - i, cur_y, ' ' | cur_attr);
		}
		dirty.set(cur_y);
		break;
	 }

	 case 'r':		// DECSTBM
	 {
		// CSI <t1> ; <b1> r。
		// スクロール範囲を [t1, b1] に設定。
		int t1 = GetArg();
		int b1 = GetArg();
		if (t1 == 0 && b1 == 0) {
			// 省略 (0;0) なら解除。
			putlog(2, "DECSTBM (Set Top&Bottom Margin) Clear");
			ResetScroll();
		} else {
			if (t1 == 0) {
				t1 = 1;
			}
			if (b1 == 0) {
				b1 = 1;
			}
			putlog(2, "DECSTBM (Set Top&Bottom Margin) %u; %u", t1, b1);
			int top = t1 - 1;
			int btm = b1 - 1;
			// 無効値なら変更しない。
			if (top < 0 || top >= height) {
				break;
			}
			if (btm < 0 || btm >= height) {
				break;
			}
			if (top >= btm) {
				break;
			}
			scroll_top = top;
			scroll_btm = btm;
		}
		// そのホームポジションに移動。
		LocateXinMargin(0);
		LocateYinScroll(0);
		break;
	 }

	 case 'X':		// ECH (Erase Character)
	 {
		int n = GetArgDefault(1);
		putlog(3, "ECH (Erase Char) %u", n);
		int maxlen = margin_right - cur_x + 1;
		n = std::min(n, maxlen);
		for (int i = 0; i < n; i++) {
			Setchar(cur_x + i, cur_y, ' ' | cur_attr);
		}
		dirty.set(cur_y);
		break;
	 }

	 case 'y':		// DECTST (Tests)
		return Unimpl(ch, "DECTST");

	 case '?':
		seq.push_back(ch);
		state = ch;
		return true;

	 default:
		return false;
	}

	ResetState();
	return true;
}

// CSI '?' の次の文字を処理する。
// 処理すれば true、知らないシーケンスなので中止する場合は false を返す。
bool
ConsoleDevice::PutcharCSIQ(uint32 ch)
{
	switch (ch) {
	 case '\x00'
	  ... '\x17':
	 case '\x19':	// EM
	 case '\x1c':	// FS (File Separator)
	 case '\x1d':	// GS (Group Separator)
	 case '\x1e':	// RS (Record Separator)
	 case '\x1f':	// US (Unit Separator)
	 case '\x7f':	// DEL
		// 制御文字はその場で実行される。
		PutcharPlain(ch);
		// ステート完了ではないので break ではなく return。
		return true;

	 case '\x18':	// CAN
	 case '\x1a':	// SUB
		// Cancel
		break;

	 case ESC:		// ESC
		// ここまでのを捨てて新しく始める?らしい。
		ResetState();
		PutcharPlain(ch);
		return true;

	 case '0':
	 case '1':
	 case '2':
	 case '3':
	 case '4':
	 case '5':
	 case '6':
	 case '7':
	 case '8':
	 case '9':
	 case ';':
	 case '\"':
	 case '$':
	 case '>':
		seq.push_back(ch);
		arg.push_back(ch);
		return true;

	 case 'h':		// DECSET (拡張オプションを設定)
	 case 'l':		// DECRST (拡張オプションを解除)
	 {
		// CSI '?' <n> h
		// CSI '?' <n> l
		bool val = (ch == 'h');
		int argc = ParseArg();
		for (int i = 0; i < argc; i++) {
			int n = GetArg();
			switch (n) {
			 case 1:	// DECCKM
				// 上下左右キーの送信コード設定。
				if (val) {
					putlog(3, "Set DECCKM (Cursor App. Keycode)");
				} else {
					putlog(3, "Reset DECCKM (Cursor Keycode)");
				}
				keyboard->SetCKM(val);
				break;

			 case 2:	// DECANM (解除のみ)
				return Unimpl(ch, "DECANM");

			 case 3:	// DECCOLM (オンで 132 桁/オフで80桁)
				// 内部的に桁数を切り替える。(表示は80桁固定)
				if (val) {
					putlog(2, "Set DECCOLM (to 132 columns)");
					width = 132;
				} else {
					putlog(2, "Reset DECCOLM (to 80 columns)");
					width = 80;
				}
				ResetMargin();
				ResetScroll();
				Clear();
				Locate(0, 0);
				break;

			 case 4:	// DECSCLM (オンでスムーズスクロール)
				putlog(3, "%s DECSCLM (Smooth/Jump Scroll)",
					val ? "Set" : "Reset");
				// 対応不要?
				break;

			 case 5:	// DECSCNM (オンで明るい背景…)
				return Unimpl(ch, "DECSCNM");

			 case 6:	// DECOM
				putlog(3, "%s DECOM(Origin Mode)", val ? "Set" : "Reset");
				decom = val;
				break;

			 case 7:	// DECAWM (Auto Wrap)
				return Unimpl(ch, "DECAWM");
			 case 8:	// DECARM (Auto Repeat)
				return Unimpl(ch, "DECARM");
			 case 18:	// DECPFF (Print Form Feed)
				return Unimpl(ch, "DECPFF");
			 case 19:	// DECPEX (オンで MC/DECMC が DECSTBM の影響を受けない)
				return Unimpl(ch, "DECPEX");

			 case 25:	// DECTCEM
				putlog(3, "%s DECTCEM(Cursor Visible)", val ? "Set" : "Reset");
				cursor_visible = val;
				break;

			 case 42:	// DECNRCM (Character set National/Multinational)
				return Unimpl(ch, "DECRCM");

			 case 68008:
				// パフォーマンス測定用のダミー。黙って無視する。
				break;

			 default:
				// 知らないのは無視。
				putlog(2, "Ignore private option %u %c", n, ch);
				break;
			}
		}
		break;
	 }

	 case 'i':		// Printing
		return Unimpl(ch, "Printing");
	 case 'J':		// DECSED (Selective Erase In Display)
		return Unimpl(ch, "DECSED");
	 case 'K':		// DECSEL (Selective Erase In Line)
		return Unimpl(ch, "DECSEL");

	 case 'n':		// DECDSR
	 {
		int n = GetArg();
		switch (n) {
		 case 15:	// Request for printer status
			// プリンタの状態問い合わせ。
			// ここではプリンタなしを応答しておく。
			putlog(3, "DECDSR 15(Request Printer Status)");
			SendString("\x1b[?13n");
			break;

		 case 25:	// Request for UDK(User-Defined Key) status
			// UDK は実装していないので、常時初期値(Unlocked = 20) を返す。
			putlog(3, "DECDSR 25 (Request UDK Status)");
			SendString("\x1b[?20n");
			break;

		 case 26:	// Request for keyboard language
			// どの国(言語)のキーボードかを返す。
			// とりあえず North America。
			putlog(3, "DECDSR 26(Request Keyboard Language)");
			SendString("\x1b[?27;1n");
			break;

		 default:
			return false;
		}
		break;
	 }

	 default:
		return false;
	}

	ResetState();
	return true;
}

// ESC '#' の次の文字を処理する。
bool
ConsoleDevice::PutcharSharp(uint32 ch)
{
	switch (ch) {
	 case '3':		// DECDHL 上
		return Unimpl(ch, "DECDHL(hi)");
	 case '4':		// DECDHL 下
		return Unimpl(ch, "DECDHL(lo)");
	 case '5':		// DECSWL
		return Unimpl(ch, "DECSWL");
	 case '6':		// DECDWL
		return Unimpl(ch, "DECDWL");

	 case '8':		// DECALN (Screen Alignment Test)
		// 画面を全部 'E' で埋める。
		// メンテ用なのでたぶん属性無視だろうか。
		putlog(3, "DECALN (Screen Alignment Tests)");
		std::fill(screen.begin(), screen.end(), 'E');
		dirty.set();
		break;

	 default:
		return false;
	}

	ResetState();
	return true;
}

// ESC '(' (G0) の次の文字を処理する。
// 処理すれば true、知らないシーケンスなので中止する場合は false を返す。
bool
ConsoleDevice::PutcharG0(uint32 ch)
{
	switch (ch) {
	 case 'B':		// ASCII
		putlog(3, "G0 ASCII");
		SetAttr(cur_attr & ~ATTR_DECSG);
		break;

	 case '0':		// DEC special graphics
		putlog(3, "G0 DEC Special Graphics)");
		SetAttr(cur_attr | ATTR_DECSG);
		break;

	 // 各国語モード。同時にいずれか一つが有効らしい。いずれも対応しない。
	 case 'A':		// British
	 case '4':		// Dutch
	 case 'C':		// Finnish
	 case '5':		// Finnish
	 case 'R':		// French
	 case 'Q':		// French canadian
	 case 'K':		// German
	 case 'Y':		// Italian
	 case 'E':		// Norwegian/Danish
	 case '6':		// Norwegian/Danish
	 case 'Z':		// Spanish
	 case 'H':		// Swedish
	 case '7':		// Swedish
	 case '=':		// Swiss
		putlog(2, "Ignore national mode:%s", Dump(ch).c_str());
		break;

	 case '<':		// DEC Supplemental
		return Unimpl(ch, "DEC Supplemental");

	 default:
		// VT220 にはない。
		return false;
	}

	ResetState();
	return true;
}

// 未実装シーケンスのログ表示をする。
// 処理済みを表す true を返す。
bool
ConsoleDevice::Unimpl(uint32 ch, const char *name)
{
	std::string msg = Dump(ch);
	if (name) {
		msg += " (";
		msg += name;
		msg += ')';
	}
	putlog(1, "%s (NOT IMPLEMENTED)", msg.c_str() + 1);

	ResetState();
	return true;
}

// argv を一つ取り出して整数にして返す。
// 要素数が足りない場合、パラメータが空の場合、パラメータが数値に変換できない
// 場合はいずれも defval を返す。
// それをどう解釈するかは各シーケンスによる。
int
ConsoleDevice::GetArgDefault(int defval)
{
	if (argv_idx < 0) {
		ParseArg();
	}

	// パラメータがない。
	if (argv_idx >= argv.size()) {
		return defval;
	}
	const std::string& a = argv[argv_idx++];

	// 空文字列。
	if (a.empty()) {
		return defval;
	}

	// '-' は arg に追加されないため負数にはならない。
	char *end;
	errno = 0;
	long lval = strtol(a.c_str(), &end, 10);
	if (errno == ERANGE || *end != '\0') {
		return defval;
	}

	if (lval == 0) {
		return defval;
	}
	return (int)lval;
}

// arg をセミコロンで分解して argv に代入(追加)する。
// argv の要素数を返す。
int
ConsoleDevice::ParseArg()
{
	const char *s = arg.c_str();
	while (*s) {
		auto pos = s - arg.c_str();
		const char *sep = strchr(s, ';');
		if (sep) {
			auto len = sep - s;
			argv.emplace_back(arg, pos, len);
			s = sep + 1;
		} else {
			argv.emplace_back(arg, pos);
			break;
		}
	}

	argv_idx = 0;
	return argv.size();
}

// 現在の seq を表示用文字列にして返す。
std::string
ConsoleDevice::Dump() const
{
	return Dump(seq);
}

// 現在の seq と ch を表示用文字列にして返す。
// 大抵の場合最後の1文字はまだ seq に入っていないのでこの形式があると便利。
std::string
ConsoleDevice::Dump(uint32 ch) const
{
	std::vector<uint8> tmp = seq;
	tmp.push_back(ch);
	return Dump(tmp);
}

// エスケープシーケンスを表示用文字列にして返す。
// 戻り文字列には先頭に空白が入っている。
/*static*/ std::string
ConsoleDevice::Dump(const std::vector<uint8>& src)
{
	std::string dst;

	for (auto ch : src) {
		if (ch == ESC) {
			dst += " ESC";
		} else if (ch < ' ' || ch >= 0x7f) {
			dst += string_format(" \\x%02x", ch);
		} else if (ch == ' ') {
			dst += " ' '";
		} else {
			dst += ' ';
			dst += (char)ch;
		}
	}
	return dst;
}

// 画面を消去する。
// カーソル位置は変わらない。属性もクリアされない。
void
ConsoleDevice::Clear()
{
	std::fill(screen.begin(), screen.end(), ' ' | cur_attr);
	dirty.set();
}

// カーソルの X 座標を x に移動する。
// x が物理画面外なら画面の左端か右端にクリップされる。
// 左右マージンの影響は受けない。
void
ConsoleDevice::LocateX(int x)
{
	wrap_pending = false;

	// 物理画面内 (左右マージンではなく) にクリップ。
	if (__predict_false(x < 0)) {
		x = 0;
	}
	if (__predict_false(x > width - 1)) {
		x = width - 1;
	}

	cur_x = x;
	dirty.set(cur_y);
}

// カーソルの Y 座標を y に移動する。
// y が物理画面外なら画面の上端か下端にクリップされる。
// スクロール領域の影響は受けない。
void
ConsoleDevice::LocateY(int y)
{
	wrap_pending = false;

	// 物理画面内 (スクロール領域ではなく) にクリップ。
	if (__predict_false(y < 0)) {
		y = 0;
	}
	if (__predict_false(y > height - 1)) {
		y = height - 1;
	}

	dirty.set(cur_y);
	cur_y = y;
	dirty.set(cur_y);
}

// カーソルの X 座標を x に移動する。
// x は常に画面左から数えるが、
// 左右マージンを超える場合はマージンの左端か右端にクリップされる。
void
ConsoleDevice::LocateXinMargin(int x)
{
	if (__predict_false(x < margin_left)) {
		x = margin_left;
	}
	if (__predict_false(x > margin_right)) {
		x = margin_right;
	}
	LocateX(x);
}

// カーソルの Y 座標を y に移動する。
// y は常に画面全体で数えるが、
// スクロール領域を超える場合はスクロール領域の上端か下端にクリップされる。
void
ConsoleDevice::LocateYinScroll(int y)
{
	if (__predict_false(y < scroll_top)) {
		y = scroll_top;
	}
	if (__predict_false(y > scroll_btm)) {
		y = scroll_btm;
	}
	LocateY(y);
}

// src 行目を dst 行にコピーする。スクロールの下請け。
// DECLRMM による左右マージンの影響は受けない。
// DECSTBM によるスクロール領域は呼び出し側で管理すること
// (基本どのスクロールも影響を受けるはず)。
void
ConsoleDevice::CopyLineWhole(int dst, int src)
{
	assertmsg(0 <= src && src < height, "src=%d", src);
	assertmsg(0 <= dst && dst < height, "dst=%d", dst);

	memcpy(&screen[dst * fullwidth], &screen[src * fullwidth],
		fullwidth * sizeof(screen[0]));
	dirty.set(dst);
}

// src 行目を dst 行にコピーする。スクロールの下請け。
// DECLRMM による左右マージン内のみコピーする。
// DECSTBM によるスクロール領域は呼び出し側で管理すること
// (基本どのスクロールも影響を受けるはず)。
void
ConsoleDevice::CopyLineMargin(int dst, int src)
{
	assertmsg(0 <= src && src < height, "src=%d", src);
	assertmsg(0 <= dst && dst < height, "dst=%d", dst);

	int cols = margin_right - margin_left + 1;
	memcpy(&screen[dst * fullwidth + margin_left],
		&screen[src * fullwidth + margin_left],
		cols * sizeof(screen[0]));
	dirty.set(dst);
}

// y 行目の内容を消去する。
// DECLRMM による左右マージンの影響は受けない。
// DECSTBM は無関係。
void
ConsoleDevice::EraseLineWhole(int y)
{
	assertmsg(0 <= y && y < height, "y=%d", y);

	for (int x = 0; x < width; x++) {
		Setchar(x, y, ' ' | cur_attr);
	}
	dirty.set(y);
}

// y 行目の内容を消去する。
// DECLRMM による左右マージンの影響を受ける。
// DECSTBM は無関係。
void
ConsoleDevice::EraseLineMargin(int y)
{
	assertmsg(0 <= y && y < height, "y=%d", y);

	for (int x = margin_left; x <= margin_right; x++) {
		Setchar(x, y, ' ' | cur_attr);
	}
	dirty.set(y);
}

// 現在位置に、現在の属性で文字 ch を出力する。
// 出力後カーソルは移動する。
void
ConsoleDevice::PutcharNormal(uint32 ch)
{
	if (insert_mode) {
		// 折り返し待ちは解除される?
		wrap_pending = false;

		// カーソル以降を1つ右へ移動して、ここに1文字出力。
		memmove(&screen[cur_y * fullwidth + (cur_x + 1)],
			&screen[cur_y * fullwidth + cur_x],
			(margin_right - cur_x) * sizeof(screen[0]));
		Setchar(cur_x, cur_y, (ch | cur_attr));
	} else {
		// 折り返し待ちの時はこの文字を表示する前に改行。
		if (wrap_pending) {
			// wrap_pending は CR() で解除される。
			CR();
			LF();
		}

		// 1文字出力して、1つ右へ移動する。
		Setchar(cur_x, cur_y, (ch | cur_attr));
		cur_x++;

		// マージンの最終桁にいた場合は wrap pending を立てて、留まる。
		if (cur_x > margin_right) {
			wrap_pending = true;
			cur_x--;
		}
	}

	dirty.set(cur_y);
}

// (x, y) の位置に chattr を出力する。
// 出力後カーソルは移動しない。
// 現在の attr は参照しない (呼び出す側が指定すること)。
void
ConsoleDevice::Setchar(int x, int y, uint32 chattr)
{
	int pos = y * fullwidth + x;
	assert(pos >= 0);
	assert(pos < screen.size());
	screen[pos] = chattr;
}

// TAB。
// カーソルを次のタブ位置に移動。
void
ConsoleDevice::HT()
{
	int x = cur_x;
	int tabstop = ((x / 8) + 1) * 8;
	if (tabstop > margin_right) {
		tabstop = margin_right;
	}
	for (; x < tabstop; x++) {
		PutcharNormal(' ');
	}
}

// 復帰。
// DECLRMM による左右マージンの影響を受ける。
void
ConsoleDevice::CR()
{
	LocateXinMargin(margin_left);
}

// 改行。
// DECLRMM による左右マージンの影響を受ける。
// DECSTBM によるスクロール領域の影響を受ける。
void
ConsoleDevice::LF()
{
	wrap_pending = false;

	dirty.set(cur_y);
	cur_y++;

	// スクロール領域下端を出るなら画面をスクロールアップ。
	if (cur_y > scroll_btm) {
		for (int y = scroll_top + 1; y <= scroll_btm; y++) {
			CopyLineMargin(y - 1, y);
		}
		EraseLineMargin(scroll_btm);
		cur_y = scroll_btm;
	}

	// New Line Mode なら LF/VT/FF は行頭へ戻る。
	if (cr_on_lf) {
		CR();
	}

	dirty.set(cur_y);
}

// 現在の属性を設定する。
// ほぼログ出力のため。
void
ConsoleDevice::SetAttr(uint32 new_attr)
{
	if (cur_attr == new_attr) {
		return;
	}
	cur_attr = new_attr;

	if (log) {
		std::string buf;
		if ((cur_attr & ATTR_BOLD)) {
			buf += ",ATTR_BOLD";
		}
		if ((cur_attr & ATTR_ITALIC)) {
			buf += ",ATTR_ITALIC";
		}
		if ((cur_attr & ATTR_UNDERLINE)) {
			buf += ",ATTR_UNDERLINE";
		}
		if ((cur_attr & ATTR_STRIKE)) {
			buf += ",ATTR_STRIKE";
		}
		if ((cur_attr & ATTR_OVERLINE)) {
			buf += ",ATTR_OVERLINE";
		}
		if ((cur_attr & ATTR_DECSG)) {
			buf += ",ATTR_DECSG";
		}
		if ((cur_attr & ATTR_REVERSE)) {
			buf += ",ATTR_REVERSE";
		}

		if (buf.empty()) {
			buf = "<ATTR_NORMAL>";
		} else {
			buf[0] = '<';
			buf += '>';
		}
		fputs(buf.c_str(), log);
		fflush(log);
	}
}

// 文字列を送信。終端の '\0' は送らない。
void
ConsoleDevice::SendString(const std::string& str)
{
	if (__predict_false(loglevel >= 2)) {
		// Dump() は vector を受け取るので…。
		std::vector<uint8> vec(str.length());
		for (int i = 0, end = str.length(); i < end; i++) {
			vec[i] = str[i];
		}
		putlogn("SendString%s", Dump(vec).c_str());
	}

	if (comdriver) {
		for (auto ch : str) {
			comdriver->EnqueueChar(ch);
		}
	}
}

// 現在の画面を文字列にして返す。
std::string
ConsoleDevice::ToString() const
{
	std::string dump;

	for (int y = 0; y < height; y++) {
		const uint32 *s = &screen[y * fullwidth];
		const uint32 *e = s + width - 1;
		// 行末方向から空白でない文字を探す。
		for (; e >= s; e--) {
			uint8 ch = *e & 0xff;
			if (ch != ' ') {
				break;
			}
		}
		// 行頭からそこまでをコピー。
		for (; s <= e; s++) {
			uint8 ch = *s & 0xff;
			dump += (char)ch;
		}
		// 論理的に折り返してるかどうかの区別はつかない。
		dump += '\n';
	}

	return dump;
}

// 画面合成。
// レンダリングスレッドから呼ばれる。
bool
ConsoleDevice::Render(BitmapRGBX& dst)
{
	BitmapRGBX row(width * font_width, font_height);
	bool updated = false;

	// フチも含めて再描画。
	if (__predict_false(init_screen)) {
		init_screen = false;
		dst.Fill(palette[0]);
		updated = true;
	}

	// 行ごとに更新があるか調べる。
	bitsetH modified;
	{
		std::lock_guard<std::mutex> lock(mtx);
		modified = pending;
		pending.reset();
	}
	updated |= modified.any();

	const uint8 *cgrom8x16 = Builtin::CGROM8x16(0);
	for (uint y = 0; y < height; y++) {
		if (__predict_false(modified.test(y))) {
			row.Fill(palette[0]);
			const uint32 *s = &screen[y * fullwidth];
			for (uint x = 0; x < width; x++) {
				uint32 attr = *s++;
				uint32 ch = attr & 0xff;
				// 属性による反転。
				int paloff = 0;
				if ((attr & ATTR_REVERSE) != 0) {
					paloff ^= 1;
				}
				// カーソル位置は更に反転。
				if (cursor_visible && x == cur_x && y == cur_y) {
					paloff ^= 1;
				}

				// 点滅の滅の時はグリフを空白に差し替えて描画。
				if (__predict_false(blinking) && (attr & ATTR_BLINK)) {
					ch = ' ';
					attr &= ~ATTR_LINEMASK;
				}

				const uint8 *font;
				if (__predict_false(ch >= 0x80)) {
					font = &tofu8x16[0];
				} else if ((attr & ATTR_DECSG) && ('j' <= ch && ch <= 'x')) {
					font = &decsg8x16[(ch - 'j') * font_height];
				} else {
					// 0x5c は CGROM では円記号なのでバックスラッシュ(0x80)
					// にする。同様に 0x7e の ' ̄'(オーバーライン)をチルダ
					// (0x81) にする。
					if (ch == '\\') {
						ch = 0x80;
					} else if (ch == '~') {
						ch = 0x81;
					}
					font = &cgrom8x16[ch * font_height];
				}
				BitmapI1 bc1(font, font_width, font_height);
				if (__predict_false((attr & ATTR_ITALIC))) {
					bc1 = bc1.ConvertToItalic();
				}
				if (__predict_false((attr & ATTR_BOLD))) {
					bc1 = bc1.ConvertToBold();
				}

				uint px = x * font_width;
				row.DrawBitmapI1(px, 0, bc1, &palette[paloff]);

				// 下線、上線、取り消し線
				if (__predict_false((attr & ATTR_LINEMASK))) {
					Color fg = palette[paloff ^ 1];
					if ((attr & ATTR_OVERLINE)) {
						row.DrawLineH(fg, px, 0, px + font_width);
					}
					if ((attr & ATTR_STRIKE)) {
						row.DrawLineH(fg, px, 9, px + font_width);
					}
					if ((attr & ATTR_UNDERLINE)) {
						row.DrawLineH(fg, px, 14, px + font_width);
					}
				}
			}

			Rect rect(Padding, Padding + y * font_height,
				dst.GetWidth() - Padding * 2, font_height);
			dst.DrawBitmapStretch(rect, row);
		}
	}

	return updated;
}

#define B(x)	(0b##x)
#define B2(x)	(B(x) >> 8), (B(x) & 0xff)

// DEC Special Graphics Character Set フォント。(罫線のみ)
/*static*/ const uint8 ConsoleDevice::decsg8x16[15 * font_height] = {
	B(00010000),	// [0] j:┘
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(11110000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),

	B(00000000),	// [1] k:┐
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(11110000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),

	B(00000000),	// [2] l:┌
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00011111),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),

	B(00010000),	// [3] m:└
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00011111),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),

	B(00010000),	// [4] n:┼
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(11111111),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),

	B(00000000),	// [5] o:─(Scan1)
	B(11111111),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),

	B(00000000),	// [6] p:─(Scan3)
	B(00000000),
	B(00000000),
	B(00000000),
	B(11111111),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),

	B(00000000),	// [7] q:─(Scan5)
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(11111111),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),

	B(00000000),	// [8] r:─(Scan7)
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(11111111),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),

	B(00000000),	// [9] s:─(Scan9)
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(11111111),
	B(00000000),
	B(00000000),

	B(00010000),	// [10] t:├
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00011111),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),

	B(00010000),	// [11] u:┤
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(11110000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),

	B(00010000),	// [12] v:┴
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(11111111),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),

	B(00000000),	// [13] w:┬
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(00000000),
	B(11111111),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),

	B(00010000),	// [14] x:│
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
	B(00010000),
};

// 豆腐
/*static*/ const uint8 ConsoleDevice::tofu8x16[font_height] = {
	B(00000000),
	B(11111110),
	B(10000010),
	B(10000110),
	B(10000110),
	B(10001010),
	B(10001010),
	B(10010010),
	B(10010010),
	B(10100010),
	B(10100010),
	B(11000010),
	B(11000010),
	B(10000010),
	B(11111110),
	B(00000000),
};


//
// 入力担当デバイス
//

// コンストラクタ
ConsoleKeyboard::ConsoleKeyboard()
	: inherited()
{
	// Console 側から初期化されるはずだが、念のため。
	SetCKM(false);
}

// デストラクタ
ConsoleKeyboard::~ConsoleKeyboard()
{
}

// COM ドライバを接続。
// (HostCOM の cons ドライバが呼ぶ)
void
ConsoleKeyboard::Attach(COMDriverConsole *comdriver_)
{
	comdriver = comdriver_;
}

// 文字入力。
// WXMainView から呼ばれる。
void
ConsoleKeyboard::CharInput(uint charcode)
{
	if (comdriver == NULL) {
		return;
	}

	// charcode は 0x80 以上を独自に使っているので (keyboard.h 参照)、
	// それらをシリアルコンソールのエスケープに置き換えて送り込む。
	switch (charcode) {
	 case CC_NUL:
		comdriver->EnqueueChar(0);
		break;

	 case CC_F1 ... CC_F10:
		break;

	 // 上下左右は Normal Cursor Mode なら ESC [ A/B/C/D、
	 // Application Cursor Mode なら ESC O A/B/C/D を送る。
	 // 拡張オプション 1 (DECCKM) で切り替える。
	 case CC_up:
		comdriver->EnqueueChar(ESC);
		comdriver->EnqueueChar(ckm_char);
		comdriver->EnqueueChar('A');
		break;

	 case CC_down:
		comdriver->EnqueueChar(ESC);
		comdriver->EnqueueChar(ckm_char);
		comdriver->EnqueueChar('B');
		break;

	 case CC_right:
		comdriver->EnqueueChar(ESC);
		comdriver->EnqueueChar(ckm_char);
		comdriver->EnqueueChar('C');
		break;

	 case CC_left:
		comdriver->EnqueueChar(ESC);
		comdriver->EnqueueChar(ckm_char);
		comdriver->EnqueueChar('D');
		break;

	 default:
		comdriver->EnqueueChar(charcode);
		break;
	}
}

// DECCKM を設定/解除する。
void
ConsoleKeyboard::SetCKM(bool val)
{
	if (val) {
		ckm_char = 'O';
	} else {
		ckm_char = '[';
	}
}

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.