--- nono/vm/console.cpp 2026/04/29 17:05:32 1.1.1.1 +++ nono/vm/console.cpp 2026/04/29 17:05:59 1.1.1.6 @@ -5,21 +5,106 @@ // // -// メイン画面をコンソールにする +// メイン画面を 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 (?) +// 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 "wxcolor.h" +#include "syncer.h" #include // エスケープシーケンス等。 #define ESC '\x1b' -#define CSI '[' #define PRINTLOG(fmt...) do { \ if (log) { \ @@ -28,20 +113,12 @@ } \ } while (0) -// v[] がいずれかでも true なら true を返す。 -static bool -AnyOf(const std::vector& v) -{ - return std::any_of(v.begin(), v.end(), [](bool x) { return x; }); -} - // コンストラクタ ConsoleDevice::ConsoleDevice() : inherited(OBJ_CONSOLE) { - screen.resize(width * height); - dirty.resize(height); - pending.resize(height); + screen.resize(fullwidth * height); + SetPalette(false); } // デストラクタ @@ -51,42 +128,47 @@ ConsoleDevice::~ConsoleDevice() fclose(log); log = NULL; } + + if (comdriver) { + comdriver->DetachConsole(); + comdriver = NULL; + } } // 初期化 bool ConsoleDevice::Init() { - if (inherited::Init() == false) { - return false; - } - - renderer = GetRenderer(); + renderer = GetVideoRenderer(); + keyboard = gMainApp.GetObject(OBJ_KEYBOARD); + syncer = GetSyncer(); // ログファイル。 if (gMainApp.console_logfile) { log = fopen(gMainApp.console_logfile, "w"); if (log == NULL) { - warn("console logfile '%s'", gMainApp.console_logfile); + warn("console logfile \"%s\"", gMainApp.console_logfile); return false; } } - vsync_event.func = ToEventCallback(&ConsoleDevice::VSyncCallback); - vsync_event.time = 16666.667_usec; - vsync_event.SetName("Console V-Sync"); - scheduler->RegistEvent(vsync_event); + auto evman = GetEventManager(); + vsync_event = evman->Regist(this, + ToEventCallback(&ConsoleDevice::VSyncCallback), + "Console V-Sync"); + vsync_event->time = 16666.667_usec; return true; } // COM ドライバを接続。 -// NULL を指定すると解除。 // (HostCOM の cons ドライバが呼ぶ) void -ConsoleDevice::Attach(COMDriver *comdriver_) +ConsoleDevice::Attach(COMDriverConsole *comdriver_) { comdriver = comdriver_; + + SetPalette((comdriver != NULL)); } void @@ -101,32 +183,49 @@ ConsoleDevice::ResetHard(bool poweron) Locate(0, 0); } - if (comdriver) { - scheduler->RestartEvent(vsync_event); - } + scheduler->RestartEvent(vsync_event); - ResetTerminal(); + ResetTerminalHard(); } void -ConsoleDevice::VSyncCallback(Event& ev) +ConsoleDevice::VSyncCallback(Event *ev) { bool update_needed; - // 更新 (dirty[]) があれば pending[] に重ねる。 - { - std::unique_lock lock(mtx); - for (int y = 0; y < height; y++) { - // std::vector には operator|=() がない。 - if (__predict_false(dirty[y])) { - pending[y] = true; - dirty[y] = false; - } + // 更新のあった行については、点滅があるか全て調べ直す。 + 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); })); } + } - update_needed = AnyOf(pending); + // 点滅を持つ行は周期ごとに 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 lock(mtx); + pending |= dirty; + update_needed = pending.any(); + } + + dirty.reset(); + // 更新の必要があれば Render に作画指示。 if (update_needed) { renderer->NotifyRender(); @@ -135,17 +234,79 @@ ConsoleDevice::VSyncCallback(Event& ev) 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 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::ResetTerminal() +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; - save_curx = 0; - save_cury = 0; } // エスケープシーケンスの状態を初期化する。 @@ -155,6 +316,8 @@ ConsoleDevice::ResetState() state = 0; seq.clear(); arg.clear(); + argv.clear(); + argv_idx = -1; } // ch をログ出力する。 @@ -195,6 +358,7 @@ ConsoleDevice::Putchar(uint32 ch) PutcharDebug(ch); } + // 処理したら return。処理できなかったら switch を抜ける。 switch (state) { case ESC: if (PutcharESC(ch)) { @@ -202,31 +366,17 @@ ConsoleDevice::Putchar(uint32 ch) } break; - case CSI: + case '[': // CSI if (PutcharCSI(ch)) { return; } break; case '(': // G0 - switch (ch) { - case '0': // DEC 特殊記号 - SetAttr(cur_attr | ATTR_DECSG); - break; - case 'B': // ASCII - default: - SetAttr(cur_attr & ~ATTR_DECSG); - break; + if (PutcharG0(ch)) { + return; } - ResetState(); - return; - - case ')': // G1 - case '$': // 94 multibyte - case '#': - // とりあえず何もしない。 - ResetState(); - return; + break; case '?': // CSI '?' if (PutcharCSIQ(ch)) { @@ -234,17 +384,26 @@ ConsoleDevice::Putchar(uint32 ch) } 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()); - for (auto c : seq) { - Putc(c); - } - Putc(ch); ResetState(); } @@ -254,24 +413,31 @@ 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 - break; + // 何もしない。 + return; + case '\x07': // BEL // とりあえず無視。 - break; + putlog(2, "BEL (Ignored)"); + return; case '\x08': // BS - LocateX(cur_x - 1); + // カーソルを 1 文字左に移動。文字は消さない。 + // DECLRMM(左右マージン)の影響を受ける。 + LocateXinMargin(cur_x - 1); return; - case '\t': // TAB - // XXX 右端どうなる? - LocateX(roundup(cur_x, 8)); + case '\t': // HT + HT(); return; case '\n': // LF @@ -280,15 +446,12 @@ ConsoleDevice::PutcharPlain(uint32 ch) LF(); return; - case '\r': // CR + case '\r': // CR CR(); return; case '\x0e': // SO case '\x0f': // SI - // Change GL - break; - case '\x10': // DLE case '\x11': // DC1/XON case '\x12': // DC2 @@ -297,7 +460,8 @@ ConsoleDevice::PutcharPlain(uint32 ch) case '\x15': // NAK case '\x16': // SYN case '\x17': // ETB - break; + // 何もしない。 + return; case '\x18': // CAN // Cancel @@ -305,33 +469,37 @@ ConsoleDevice::PutcharPlain(uint32 ch) return; case '\x19': // EM - break; + // 何もしない。 + return; case '\x1a': // SUB ResetState(); return; - case ESC: + case ESC: // ESC seq.push_back(ch); state = ch; return; - case '\x1c': // FS - case '\x1d': // GS - case '\x1e': // RS - case '\x1f': // US - break; + 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: - if (ch < 0x80) { - // ここが通常の1文字出力。 - Putc(ch); - return; - } - // 8ビットシーケンスは無視? - break; + // ここが通常の1文字出力。 + // 8ビット文字はここでは素通しするが、レンダラで豆腐を出力する。 + // NetBSD の sysinst が言語選択画面で1文字送ってくる。 + // 8ビットのエスケープシーケンス (\x80〜\x9f) のことは無視する。 + PutcharNormal(ch); + return; } - putlog(2, "Ignore: \\x%02x", ch); } // ESC の次の文字を処理する。 @@ -343,65 +511,129 @@ ConsoleDevice::PutcharESC(uint32 ch) case '[': // CSI case '(': // G0 case ')': // G1 - case '$': // 94 multibyte + case '*': // G2 + case '+': // G3 case '#': // Font width/height control seq.push_back(ch); state = ch; return true; case '\\': // ST + putlog(2, "ST"); break; - case '7': // カーソル位置保存 - save_curx = cur_x; - save_cury = cur_y; + 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 '8': // カーソル位置復帰 - Locate(save_curx, save_cury); + 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 'c': // Hard terminal reset - Clear(); - ResetTerminal(); - Locate(0, 0); + case 'E': // NEL (Next Line) + // カーソルを次行の先頭に。スクロール領域下端に来たらスクロールアップ。 + // なので CR+LF と等価。 + putlog(3, "NEL (Next Line)"); + CR(); + LF(); break; - case 'M': // 1行上に移動 (画面を1行下にスクロール) - ScrollDown(); - Locate(0, 0); + 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 '*': // G2 - case '+': // G3 - case 'B': // ASCII - case 'A': // ISO Latin 1 - case '<': // - case '0': // DEC special graphics - case '-': // G1 (96) - case '.': // G2 - case '/': // G3 - case '4': // Dutch - case '5': - case '6': // Norwegian/Danish - case 'C': // Finnish - case 'R': // French - case 'Q': // French canadian - case 'K': // German - case 'Y': // Italian - // "%5" とかの2文字のやつは NetBSD/x68k ite も未対応。 - case '`': - case 'n': - case '}': - case 'o': - case '|': - case '~': - case '=': - case '>': - putlog(2, "Ignore:%s", Dump(ch).c_str()); + 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 'Z': // 端末属性を応答する。 + case '$': // 94 multibyte (VT300 シリーズ以降?) default: return false; } @@ -416,6 +648,33 @@ 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': @@ -430,274 +689,501 @@ ConsoleDevice::PutcharCSI(uint32 ch) case '\"': case '$': case '>': + case '!': seq.push_back(ch); arg.push_back(ch); return true; - case 'A': + case '@': // ICH (Insert Character) { - int n = atoi(arg.c_str()); - if (n == 0) { - n = 1; + // カーソル位置に 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); } - LocateY(cur_y - n); - ResetState(); - return true; + dirty.set(cur_y); + break; } - case 'B': + case 'A': // CUU { - int n = atoi(arg.c_str()); - if (n == 0) { - n = 1; - } - LocateY(cur_y + n); - ResetState(); - return true; + // カーソルを n 行上に移動。 + // DECSTBM(スクロール領域)の影響を受ける。 + int n = GetArgDefault(1); + putlog(3, "CUU (Cursor Up) %u", n); + LocateYinScroll(cur_y - n); + break; } - case 'C': + case 'B': // CUD { - int n = atoi(arg.c_str()); - if (n == 0) { - n = 1; - } - LocateX(cur_x + n); - ResetState(); - return true; + // カーソルを下に移動。 + // DECSTBM(スクロール領域)の影響を受ける。 + int n = GetArgDefault(1); + putlog(3, "CUD (Cursor Down) %u", n); + LocateYinScroll(cur_y + n); + break; } - case 'D': + case 'C': // CUF { - int n = atoi(arg.c_str()); - if (n == 0) { - n = 1; - } - LocateX(cur_x - n); - ResetState(); - return true; + // カーソルを右に移動。 + // DECLRMM(左右マージン)の影響を受ける。 + int n = GetArgDefault(1); + putlog(3, "CUF (Cursor Forward) %u", n); + LocateXinMargin(cur_x + n); + break; } - case 'H': + case 'D': // CUB { - // CSI ; H … (x-1, y-1) にカーソルを移動。 - int y = atoi(arg.c_str()); - int x = 0; - const char *p = strchr(arg.c_str(), ';'); - if (p) { - x = atoi(p + 1); - } - if (x > 0) { - x--; + // カーソルを左に移動。 + // 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"); } - if (y > 0) { - y--; + break; + } + + case 'f': // HVP + // HVP は VT52 互換性のため存在しており CUP と動作は同じらしい。 + case 'H': // CUP + { + // CSI ; 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); } - Locate(x, y); - ResetState(); - return true; + break; } - case 'J': + case 'J': // ED (Erase Display) { // CSI [] J - // が 0 か省略なら、カーソル位置から右下までを消去。 + // が 0 か省略なら、カーソル位置から画面右下までを消去。 // が 1 なら、カーソル位置から左上までを消去。 // が 2 なら、全画面消去。 + // DECLRMM(左右マージン)/DECSTBM(スクロール領域)の影響は受けない。 + // wrap は解除される。文字の属性も消去される。 // XXX SPA と ERM は未対応。 - int n = atoi(arg.c_str()); + wrap_pending = false; + int pos = cur_y * fullwidth + cur_x; + int n = GetArg(); switch (n) { - default: // 知らんけど + default: // 未定義値は 0 とする実装が多いらしい。知らんけど case 0: - for (int y = cur_y; y < height - 1; y++) { - int x = (y == cur_y) ? cur_x : 0; - for (; x < width; x++) { - Setc(x, y, ' ' | cur_attr); - } + 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++) { - int xend = (y == cur_y) ? cur_x : (width - 1); - for (int x = 0; x <= xend; x++) { - Setc(x, y, ' ' | cur_attr); - } + dirty.set(y); } break; case 2: - // カーソル位置は移動しないまま? - Clear(); + putlog(3, "ED (Erase Display) All"); + std::fill(screen.begin(), screen.end(), ' '); + dirty.set(); break; } - ResetState(); - return true; + break; } - case 'K': + case 'K': // EL (Erase Line) { // CSI [] K // が 0 か省略なら、カーソル位置の右側を消去。 // が 1 なら、カーソル位置の左側を消去。 // が 2 なら、カーソル行を消去。 - int n = atoi(arg.c_str()); + // DECLRMM (左右マージン) の影響は受けない。 + int n = GetArg(); int x0; int x1; switch (n) { - default: // 知らんけど + 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++) { - Setc(x, cur_y, ' ' | cur_attr); + Setchar(x, cur_y, ' ' | cur_attr); } - ResetState(); - return true; + dirty.set(cur_y); + break; + } + + case 'L': // IL (Insert Line) + { + // CSI [] 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': + case 'M': // DL (Delete Line) + { + // CSI [] 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 [] g + // が 0 (か省略)なら、カーソル位置のタブをクリア。 + // が 3 ならすべてのタブをクリア。 + return Unimpl(ch, "TBC"); + + case 'i': // Printing + return Unimpl(ch, "Printing"); + + case 'h': // SM (オプションを設定) + case 'l': // RM (オプションを解除) + { + // CSI h + // CSI 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 みたいにコロンで区切ったひとかたまり // もある。 - const char *ap = arg.c_str(); - for (;;) { - int n = atoi(ap); + 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 2: // 低輝度をセット (対応予定なし) - break; - case 3: // イタリックをセット - SetAttr(cur_attr | ATTR_ITALIC); - break; case 4: // 下線をセット SetAttr(cur_attr | ATTR_UNDERLINE); break; case 5: // 低速点滅をセット (対応予定なし) - break; - case 6: // 高速点滅をセット (対応予定なし) + SetAttr(cur_attr | ATTR_BLINK); break; case 7: // 反転をセット SetAttr(cur_attr | ATTR_REVERSE); break; - case 8: // 文字色を背景色と同じにする - break; - case 9: // 打ち消し線をセット - SetAttr(cur_attr | ATTR_STRIKE); - break; - case 10 ... 20: // フォントセットの設定? - break; - case 21: // 二重下線をセット (対応予定なし) - break; case 22: // ボールド(と低輝度)を解除 SetAttr(cur_attr & ~ATTR_BOLD); break; - case 23: // イタリックを解除 - SetAttr(cur_attr & ~ATTR_ITALIC); - break; case 24: // 下線を解除 SetAttr(cur_attr & ~ATTR_UNDERLINE); break; case 25: // 点滅を解除 - break; - case 26: // ? + SetAttr(cur_attr & ~ATTR_BLINK); break; case 27: // 反転を解除 SetAttr(cur_attr & ~ATTR_REVERSE); break; - case 28: // シークレットを解除 - break; - case 29: // 打ち消し線を解除 - SetAttr(cur_attr & ~ATTR_STRIKE); - break; - case 30 ... 37: // 文字色の設定 - break; - case 38: // 文字色(拡張)を設定 - break; - case 39: // 文字色をデフォルトに戻す - break; - case 40 ... 47: // 背景色を設定 - break; - case 48: // 背景色(拡張)を設定 + + // それ以降で拡張されたものに勝手に対応してみる。 + + case 3: // イタリックをセット + SetAttr(cur_attr | ATTR_ITALIC); break; - case 49: // 背景色をデフォルトに戻す + case 9: // 打ち消し線をセット + SetAttr(cur_attr | ATTR_STRIKE); break; - case 51: // □を重ねる (対応予定なし) + case 23: // イタリックを解除 + SetAttr(cur_attr & ~ATTR_ITALIC); break; - case 52: // ○を重ねる (対応予定なし) + case 29: // 打ち消し線を解除 + SetAttr(cur_attr & ~ATTR_STRIKE); break; case 53: // 上線をセット SetAttr(cur_attr | ATTR_OVERLINE); break; - case 54: // □○を取り消し - break; case 55: // 上線を取り消し SetAttr(cur_attr & ~ATTR_OVERLINE); break; - case 60 ... 63: // 縦線 (対応予定なし) - break; - case 64: // 二重打ち消し線 (対応予定なし) - break; - case 65: // 60-64 を取り消し - break; + + // 色も現状対応予定はないがメモも兼ねて並べておく。 + case 30 ... 37: // 文字色の設定 + case 38: // 文字色(拡張)を設定 + case 39: // 文字色をデフォルトに戻す + case 40 ... 47: // 背景色を設定 + case 48: // 背景色(拡張)を設定 + case 49: // 背景色をデフォルトに戻す case 90 ... 97: // 文字色(高輝度)を設定 - break; case 100 ... 107: // 背景色(高輝度)を設定 - break; 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"); + } + } - if (*ap == '\0') { + 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 ; 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; } - ap = strchr(ap + 1, ';'); - if (ap == NULL) { + if (top >= btm) { break; } - ap++; + scroll_top = top; + scroll_btm = btm; } - - ResetState(); - return true; + // そのホームポジションに移動。 + LocateXinMargin(0); + LocateYinScroll(0); + break; } - case 'r': + case 'X': // ECH (Erase Character) { - // CSI ; r。 省略時は 1。 省略時は画面最下行。 - int top = atoi(arg.c_str()); - int btm = height; - const char *p = strchr(arg.c_str(), ';'); - if (p) { - btm = atoi(p + 1); - } - if (top > 0) { - top--; - } - if (btm > 0) { - btm--; + 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); } - scroll_top = top; - scroll_btm = btm; - ResetState(); - return true; + dirty.set(cur_y); + break; } + case 'y': // DECTST (Tests) + return Unimpl(ch, "DECTST"); + case '?': seq.push_back(ch); state = ch; @@ -706,6 +1192,9 @@ ConsoleDevice::PutcharCSI(uint32 ch) default: return false; } + + ResetState(); + return true; } // CSI '?' の次の文字を処理する。 @@ -714,6 +1203,30 @@ 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': @@ -732,18 +1245,283 @@ ConsoleDevice::PutcharCSIQ(uint32 ch) arg.push_back(ch); return true; - case 'h': // 拡張オプションを設定 - case 'l': // 拡張オプションを解除 + case 'h': // DECSET (拡張オプションを設定) + case 'l': // DECRST (拡張オプションを解除) + { // CSI '?' h // CSI '?' l - // 全部未対応。 - putlog(2, "Ignored:%s", Dump(ch).c_str()); - ResetState(); - return true; + 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 を表示用文字列にして返す。 @@ -791,128 +1569,239 @@ void ConsoleDevice::Clear() { std::fill(screen.begin(), screen.end(), ' ' | cur_attr); - std::fill(dirty.begin(), dirty.end(), true); + dirty.set(); } -// カーソルの X 座標を移動。(画面をはみ出さないようにクリップされる) +// カーソルの 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[cur_y] = true; + dirty.set(cur_y); } -// カーソルの 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[cur_y] = true; + + dirty.set(cur_y); cur_y = y; - dirty[cur_y] = true; + dirty.set(cur_y); } -// 文字出力後のカーソル移動。 +// カーソルの X 座標を x に移動する。 +// x は常に画面左から数えるが、 +// 左右マージンを超える場合はマージンの左端か右端にクリップされる。 void -ConsoleDevice::Ascend() +ConsoleDevice::LocateXinMargin(int x) { - cur_x++; - dirty[cur_y] = true; - if (cur_x >= width) { - CRLF(); + 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::CR() +ConsoleDevice::LocateYinScroll(int y) { - LocateX(0); + if (__predict_false(y < scroll_top)) { + y = scroll_top; + } + if (__predict_false(y > scroll_btm)) { + y = scroll_btm; + } + LocateY(y); } -// 改行。(DECSTBM の影響を受ける) +// src 行目を dst 行にコピーする。スクロールの下請け。 +// DECLRMM による左右マージンの影響は受けない。 +// DECSTBM によるスクロール領域は呼び出し側で管理すること +// (基本どのスクロールも影響を受けるはず)。 void -ConsoleDevice::LF() +ConsoleDevice::CopyLineWhole(int dst, int src) { - dirty[cur_y] = true; - cur_y++; - if (cur_y >= scroll_btm) { - ScrollUp(); - cur_y = scroll_btm; - } - dirty[cur_y] = true; + 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); } -// 全画面を1行上にスクロール。(DECSTBM の影響を受ける) -// カーソル位置は移動しない。 +// src 行目を dst 行にコピーする。スクロールの下請け。 +// DECLRMM による左右マージン内のみコピーする。 +// DECSTBM によるスクロール領域は呼び出し側で管理すること +// (基本どのスクロールも影響を受けるはず)。 void -ConsoleDevice::ScrollUp() +ConsoleDevice::CopyLineMargin(int dst, int src) { - int dst = scroll_top * width; - int lines = scroll_btm - scroll_top; - memmove(&screen[dst], &screen[dst + width], - lines * width * sizeof(screen[0])); - uint32 ch = ' ' | cur_attr; + 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++) { - Setc(x, scroll_btm, ch); + Setchar(x, y, ' ' | cur_attr); } - std::fill(dirty.begin(), dirty.end(), true); + dirty.set(y); } -// 全画面を1行下にスクロール。(DECSTBM の影響を受ける) -// カーソル位置は移動しない。 +// y 行目の内容を消去する。 +// DECLRMM による左右マージンの影響を受ける。 +// DECSTBM は無関係。 void -ConsoleDevice::ScrollDown() +ConsoleDevice::EraseLineMargin(int y) { - int src = scroll_top * width; - int lines = scroll_btm - scroll_top; - memmove(&screen[src + width], &screen[src], - lines * width * sizeof(screen[0])); - uint32 ch = ' ' | cur_attr; - for (int x = 0; x < width; x++) { - Setc(x, scroll_top, ch); + assertmsg(0 <= y && y < height, "y=%d", y); + + for (int x = margin_left; x <= margin_right; x++) { + Setchar(x, y, ' ' | cur_attr); } - std::fill(dirty.begin(), dirty.end(), true); + dirty.set(y); } // 現在位置に、現在の属性で文字 ch を出力する。 // 出力後カーソルは移動する。 void -ConsoleDevice::Putc(uint32 ch) +ConsoleDevice::PutcharNormal(uint32 ch) { - if (ch == '\n') { - CR(); - LF(); - return; + 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--; + } } - Setc(cur_x, cur_y, ch | cur_attr); - Ascend(); + dirty.set(cur_y); } // (x, y) の位置に chattr を出力する。 // 出力後カーソルは移動しない。 // 現在の attr は参照しない (呼び出す側が指定すること)。 void -ConsoleDevice::Setc(int x, int y, uint32 chattr) +ConsoleDevice::Setchar(int x, int y, uint32 chattr) { - int pos = y * width + x; + int pos = y * fullwidth + x; assert(pos >= 0); - assert(pos < width * height); + 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 @@ -958,15 +1847,60 @@ ConsoleDevice::SetAttr(uint32 new_attr) } } +// 文字列を送信。終端の '\0' は送らない。 +void +ConsoleDevice::SendString(const std::string& str) +{ + if (__predict_false(loglevel >= 2)) { + // Dump() は vector を受け取るので…。 + std::vector 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) { - if (comdriver == NULL) { - return false; - } - + BitmapRGBX row(width * font_width, font_height); bool updated = false; // フチも含めて再描画。 @@ -977,38 +1911,52 @@ ConsoleDevice::Render(BitmapRGBX& dst) } // 行ごとに更新があるか調べる。 - std::vector modified(height); + bitsetH modified; { - std::unique_lock lock(mtx); + std::lock_guard lock(mtx); modified = pending; - std::fill(pending.begin(), pending.end(), false); + pending.reset(); } - updated |= AnyOf(modified); + updated |= modified.any(); const uint8 *cgrom8x16 = Builtin::CGROM8x16(0); for (uint y = 0; y < height; y++) { - if (__predict_false(modified[y])) { - uint py = Padding + y * font_height; - const uint32 *s = &screen[y * width]; + if (__predict_false(modified.test(y))) { + row.Fill(palette[0]); + const uint32 *s = &screen[y * fullwidth]; for (uint x = 0; x < width; x++) { - uint px = Padding + x * font_width; uint32 attr = *s++; - // とりあえず上半分(G1)も無視。 - uint32 ch = attr & 0x7f; + uint32 ch = attr & 0xff; // 属性による反転。 int paloff = 0; if ((attr & ATTR_REVERSE) != 0) { paloff ^= 1; } // カーソル位置は更に反転。 - if (x == cur_x && y == cur_y) { + 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 ((attr & ATTR_DECSG) && ('j' <= ch && ch <= 'x')) { + 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); @@ -1019,22 +1967,27 @@ ConsoleDevice::Render(BitmapRGBX& dst) bc1 = bc1.ConvertToBold(); } - dst.DrawBitmapI1(px, py, bc1, &palette[paloff]); + 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)) { - dst.DrawLineH(fg, px, py + 0, px + font_width); + row.DrawLineH(fg, px, 0, px + font_width); } if ((attr & ATTR_STRIKE)) { - dst.DrawLineH(fg, px, py + 9, px + font_width); + row.DrawLineH(fg, px, 9, px + font_width); } if ((attr & ATTR_UNDERLINE)) { - dst.DrawLineH(fg, px, py + 14, px + font_width); + 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); } } @@ -1302,12 +2255,24 @@ ConsoleDevice::Render(BitmapRGBX& dst) B(00010000), }; -// パレット。 -// [0] からだと通常色、[1] からだと反転色になる。 -/*static*/ const Color ConsoleDevice::palette[3] = { - UD_LIGHT_GREY, // BG - UD_BLACK, // FG - UD_LIGHT_GREY, // BG +// 豆腐 +/*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), }; @@ -1319,6 +2284,8 @@ ConsoleDevice::Render(BitmapRGBX& dst) ConsoleKeyboard::ConsoleKeyboard() : inherited() { + // Console 側から初期化されるはずだが、念のため。 + SetCKM(false); } // デストラクタ @@ -1329,7 +2296,7 @@ ConsoleKeyboard::~ConsoleKeyboard() // COM ドライバを接続。 // (HostCOM の cons ドライバが呼ぶ) void -ConsoleKeyboard::Attach(COMDriverCons *comdriver_) +ConsoleKeyboard::Attach(COMDriverConsole *comdriver_) { comdriver = comdriver_; } @@ -1353,27 +2320,30 @@ ConsoleKeyboard::CharInput(uint charcode 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('['); + comdriver->EnqueueChar(ckm_char); comdriver->EnqueueChar('A'); break; case CC_down: comdriver->EnqueueChar(ESC); - comdriver->EnqueueChar('['); + comdriver->EnqueueChar(ckm_char); comdriver->EnqueueChar('B'); break; case CC_right: comdriver->EnqueueChar(ESC); - comdriver->EnqueueChar('['); + comdriver->EnqueueChar(ckm_char); comdriver->EnqueueChar('C'); break; case CC_left: comdriver->EnqueueChar(ESC); - comdriver->EnqueueChar('['); + comdriver->EnqueueChar(ckm_char); comdriver->EnqueueChar('D'); break; @@ -1382,3 +2352,14 @@ ConsoleKeyboard::CharInput(uint charcode break; } } + +// DECCKM を設定/解除する。 +void +ConsoleKeyboard::SetCKM(bool val) +{ + if (val) { + ckm_char = 'O'; + } else { + ckm_char = '['; + } +}