10. コンソール入力の編集 -- termios を使う --

Linux のコンソールからの入力行を編集するには,単に標準入力から 取り込むだけでは実現できません.キーボードから入力する文字のうち カーソルを移動したり,文字の削除などに使うキー入力は表示する必要 がありません.またカーソルを移動するための方法も必要になります. このようにコンソールに対する入力を制御するために termios という 機能を使い,エスケープシーケンスと呼ぶ画面出力をコントロール するための特定の文字列を使用する必要があります. termios とエスケープシーケンスには非常に豊富な機能がありますが, ここでは行編集に使う部分だけを解説します.

termios を制御するために termios 構造体に必要な設定をします. termios 構造体は カーネルソースで次のように定義されています. (linux-2.4.0-test6/include/asm-i386/termbits.h の場合)

    #define NCCS 19
    struct termios {
        tcflag_t c_iflag;       /* 入力モードフラグ */
        tcflag_t c_oflag;       /* 出力モードフラグ */
        tcflag_t c_cflag;       /* 制御モードフラグ */
        tcflag_t c_lflag;       /* ローカルモードフラグ */
        cc_t c_line;            /* 通信制御 */
        cc_t c_cc[NCCS];        /* 制御文字 */
    };

入力行の編集機能を持つプログラムを作成する場合に問題となるのは ローカルエコーと呼ばれるキー入力をそのまま画面に表示するような デフォルトの設定です.カーソルを移動するキー入力などは画面への エコーを停止する必要があります.そのために termios 構造体の ローカルモードフラグ (c_lflag) と制御文字 (c_cc[]) を変更しますが, プログラム終了時に設定を元に戻しておく必要があります.

全体の流れは以下のようになります.

  1. 現在の端末設定を保存
  2. 端末を新しく設定
  3. 行編集処理
  4. 終了時に端末を保存してある設定に復帰

termios.c_lflag の設定のうち ISIG, ICANON, ECHO, ECHONL を設定します. ICANON を OFF にして Raw モードとし,ローカルエコーを OFF にするため に ECHO と ECHONL を OFF にします.また,ISIG を OFF にしてキー入力に よるシグナルの送信を止めます.以上の設定で read システムコールで キーボードからの入力をそのままプログラムで取得することができるように なります.また入力を 1 バイト毎に受け取るために,端末が Raw モードのとき read システムコールが 1 バイト受け取るまでリターンしないように 制御文字の配列の内 c_cc[VMIN] を 1, c_cc[VTIME] を 0 に設定します.

制御文字の初期設定は以下のようになっています.

intr=^C quit=^\ erase=del kill=^U
eof=^D vtime=\0 vmin=\1 sxtc=\0
start=^Q stop=^S susp=^Z eol=\0
reprint=^R discard=^U werase=^W lnext=^V
eol2=\0

まず,端末の制御に必要な定数を Linux カーネルのヘッダファイルから 抽出して用意します.

;---------------------------------------------------------------------
; file          : termios.inc
; created       : 2000/08/19
; assembler     : nasm 0.98
; description   : termios structures and constants
; derived from  : linux-2.4.0-test6/include/asm-i386/termbits.h,
;               : termios.h, ioctl.h, ioctls.h
;
;Copyright (C) 2000 Jun Mizutani <[email protected]>
;---------------------------------------------------------------------

%ifndef __TERMIOS_INC
%define __TERMIOS_INC

%include "vartype.inc"

;---------------------------------------------------------------------
; /usr/src/linux/include/asm-i386/termbits.h

%assign NCCS 19

struc termios
    .c_iflag    UINT   1       ; input mode flags
    .c_oflag    UINT   1       ; output mode flags
    .c_cflag    UINT   1       ; control mode flags
    .c_lflag    UINT   1       ; local mode flags
    .c_line     UCHAR  1       ; line discipline
    .c_cc       UCHAR  NCCS    ; control characters
endstruc

;  c_cc characters
%assign VINTR       0
%assign VQUIT       1
%assign VERASE      2
%assign VKILL       3
%assign VEOF        4
%assign VTIME       5
%assign VMIN        6
%assign VSWTC       7
%assign VSTART      8
%assign VSTOP       9
%assign VSUSP       10
%assign VEOL        11
%assign VREPRINT    12
%assign VDISCARD    13
%assign VWERASE     14
%assign VLNEXT      15
%assign VEOL2       16

;  c_iflag bits
%assign IGNBRK  0000001q
%assign BRKINT  0000002q
%assign IGNPAR  0000004q
%assign PARMRK  0000010q
%assign INPCK   0000020q
%assign ISTRIP  0000040q
%assign INLCR   0000100q
%assign IGNCR   0000200q
%assign ICRNL   0000400q
%assign IUCLC   0001000q
%assign IXON    0002000q
%assign IXANY   0004000q
%assign IXOFF   0010000q
%assign IMAXBEL 0020000q

;  c_oflag bits
%assign OPOST   0000001q
%assign OLCUC   0000002q
%assign ONLCR   0000004q
%assign OCRNL   0000010q
%assign ONOCR   0000020q
%assign ONLRET  0000040q
%assign OFILL   0000100q
%assign OFDEL   0000200q
%assign NLDLY   0000400q
%assign NL0     0000000q
%assign NL1     0000400q
%assign CRDLY   0003000q
%assign CR0     0000000q
%assign CR1     0001000q
%assign CR2     0002000q
%assign CR3     0003000q
%assign TABDLY  0014000q
%assign TAB0    0000000q
%assign TAB1    0004000q
%assign TAB2    0010000q
%assign TAB3    0014000q
%assign XTABS   0014000q
%assign BSDLY   0020000q
%assign BS0     0000000q
%assign BS1     0020000q
%assign VTDLY   0040000q
%assign VT0     0000000q
%assign VT1     0040000q
%assign FFDLY   0100000q
%assign FF0     0000000q
%assign FF1     0100000q

;  c_cflag bit meaning
%assign CBAUD   0010017q
%assign B0      0000000q         ;  hang up
%assign B50     0000001q
%assign B75     0000002q
%assign B110    0000003q
%assign B134    0000004q
%assign B150    0000005q
%assign B200    0000006q
%assign B300    0000007q
%assign B600    0000010q
%assign B1200   0000011q
%assign B1800   0000012q
%assign B2400   0000013q
%assign B4800   0000014q
%assign B9600   0000015q
%assign B19200  0000016q
%assign B38400  0000017q
%assign EXTA    B19200
%assign EXTB    B38400
%assign CSIZE   0000060q
%assign CS5     0000000q
%assign CS6     0000020q
%assign CS7     0000040q
%assign CS8     0000060q
%assign CSTOPB  0000100q
%assign CREAD   0000200q
%assign PARENB  0000400q
%assign PARODD  0001000q
%assign HUPCL   0002000q
%assign CLOCAL  0004000q
%assign CBAUDEX 0010000q
%assign  B57600 0010001q
%assign B115200 0010002q
%assign B230400 0010003q
%assign B460800 0010004q
%assign B500000 0010005q
%assign B576000 0010006q
%assign B921600 0010007q
%assign B1000000 0010010q
%assign B1152000 0010011q
%assign B1500000 0010012q
%assign B2000000 0010013q
%assign B2500000 0010014q
%assign B3000000 0010015q
%assign B3500000 0010016q
%assign B4000000 0010017
%assign CIBAUD  002003600000q  ;  input baud rate (not used)
%assign CMSPAR  010000000000q  ;  mark or space (stick) parity
%assign CRTSCTS 020000000000q  ;  flow control

;  c_lflag bits
%assign ISIG    0000001q
%assign ICANON  0000002q
%assign XCASE   0000004q
%assign ECHO    0000010q
%assign ECHOE   0000020q
%assign ECHOK   0000040q
%assign ECHONL  0000100q
%assign NOFLSH  0000200q
%assign TOSTOP  0000400q
%assign ECHOCTL 0001000q
%assign ECHOPRT 0002000q
%assign ECHOKE  0004000q
%assign FLUSHO  0010000q
%assign PENDIN  0040000q
%assign IEXTEN  0100000q

;  tcflow() and TCXONC use these
%assign TCOOFF          0
%assign TCOON           1
%assign TCIOFF          2
%assign TCION           3

;  tcflush() and TCFLSH use these
%assign TCIFLUSH        0
%assign TCOFLUSH        1
%assign TCIOFLUSH       2

;  tcsetattr uses these
%assign TCSANOW         0
%assign TCSADRAIN       1
%assign TCSAFLUSH       2

;---------------------------------------------------------------------
; /usr/src/linux/include/asm-i386/termios.h

struc winsize
        .ws_row     USHORT     1
        .ws_col     USHORT     1
        .ws_xpixel  USHORT     1
        .ws_ypixel  USHORT     1
endstruc

%assign NCC 8

struc termio
        .c_iflag    USHORT     1       ; input mode flags
        .c_oflag    USHORT     1       ; output mode flags
        .c_cflag    USHORT     1       ; control mode flags
        .c_lflag    USHORT     1       ; local mode flags
        .c_line     UCHAR      1       ; line discipline
        .c_cc       UCHAR      NCC     ; control characters
endstruc

;  modem lines
%assign TIOCM_LE        0x001
%assign TIOCM_DTR       0x002
%assign TIOCM_RTS       0x004
%assign TIOCM_ST        0x008
%assign TIOCM_SR        0x010
%assign TIOCM_CTS       0x020
%assign TIOCM_CAR       0x040
%assign TIOCM_RNG       0x080
%assign TIOCM_DSR       0x100
%assign TIOCM_CD        TIOCM_CAR
%assign TIOCM_RI        TIOCM_RNG
%assign TIOCM_OUT1      0x2000
%assign TIOCM_OUT2      0x4000
%assign TIOCM_LOOP      0x8000

;  ioctl (fd, TIOCSERGETLSR, &result) where result may be as below

;  line disciplines
%assign N_TTY           0
%assign N_SLIP          1
%assign N_MOUSE         2
%assign N_PPP           3
%assign N_STRIP         4
%assign N_AX25          5
%assign N_X25           6   ; X.25 async
%assign N_6PACK         7
%assign N_MASC          8   ; Reserved for Mobitex module <[email protected]>
%assign N_R3964         9   ; Reserved for Simatic R3964 module
%assign N_PROFIBUS_FDL  10  ; Reserved for Profibus <[email protected]>
; Linux IR - https://www.cs.uit.no/~dagb/irda/irda.html
%assign N_IRDA          11
; SMS block mode - for talking to GSM data cards about SMS messages
%assign N_SMSBLOCK      12
%assign N_HDLC          13  ;  synchronous HDLC
%assign N_SYNC_PPP      14  ;  synchronous PPP

;---------------------------------------------------------------------
;      intr=^C         quit=^\         erase=del       kill=^U
;      eof=^D          vtime=\0        vmin=\1         sxtc=\0
;      start=^Q        stop=^S         susp=^Z         eol=\0
;      reprint=^R      discard=^U      werase=^W       lnext=^V
;      eol2=\0

INIT_C_CC db "\003\034\177\025\004\0\1\0\021\023\032\0\022\017\027\026\0"

;---------------------------------------------------------------------
; /usr/src/linux/include/asm-i386/ioctl.h

%assign _IOC_NRBITS     8
%assign _IOC_TYPEBITS   8
%assign _IOC_SIZEBITS   14
%assign _IOC_DIRBITS    2

%assign _IOC_NRMASK     ((1 << _IOC_NRBITS)-1)
%assign _IOC_TYPEMASK   ((1 << _IOC_TYPEBITS)-1)
%assign _IOC_SIZEMASK   ((1 << _IOC_SIZEBITS)-1)
%assign _IOC_DIRMASK    ((1 << _IOC_DIRBITS)-1)

%assign _IOC_NRSHIFT    0
%assign _IOC_TYPESHIFT  (_IOC_NRSHIFT+_IOC_NRBITS)
%assign _IOC_SIZESHIFT  (_IOC_TYPESHIFT+_IOC_TYPEBITS)
%assign _IOC_DIRSHIFT   (_IOC_SIZESHIFT+_IOC_SIZEBITS)

; Direction bits.

%assign _IOC_NONE       0
%assign _IOC_WRITE      1
%assign _IOC_READ       2

%define _IOC(dir,type,nr,size) (((dir) << _IOC_DIRSHIFT)|((type) << _IOC_TYPESHIFT)|
((nr) << _IOC_NRSHIFT)|((size) << _IOC_SIZESHIFT))

;  used to create numbers
%define _IO(type,nr)            _IOC(_IOC_NONE,(type),(nr),0)
%define _IOR(type,nr,size)      _IOC(_IOC_READ,(type),(nr),(size))
%define _IOW(type,nr,size)      _IOC(_IOC_WRITE,(type),(nr),(size))
%define _IOWR(type,nr,size) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(size))

;  used to decode ioctl numbers..
%define _IOC_DIR(nr)            (((nr) >> _IOC_DIRSHIFT) & _IOC_DIRMASK)
%define _IOC_TYPE(nr)           (((nr) >> _IOC_TYPESHIFT) & _IOC_TYPEMASK)
%define _IOC_NR(nr)             (((nr) >> _IOC_NRSHIFT) & _IOC_NRMASK)
%define _IOC_SIZE(nr)           (((nr) >> _IOC_SIZESHIFT) & _IOC_SIZEMASK)

;  ...and for the drivers/sound files...

%assign IOC_IN          (_IOC_WRITE << _IOC_DIRSHIFT)
%assign IOC_OUT         (_IOC_READ << _IOC_DIRSHIFT)
%assign IOC_INOUT       ((_IOC_WRITE|_IOC_READ) << _IOC_DIRSHIFT)
%assign IOCSIZE_MASK    (_IOC_SIZEMASK << _IOC_SIZESHIFT)
%assign IOCSIZE_SHIFT   (_IOC_SIZESHIFT)

;---------------------------------------------------------------------
; /usr/src/linux/include/asm-i386/ioctls.h
;  0x54 is just a magic number to make these relatively unique ('T')

%assign TCGETS          0x5401
%assign TCSETS          0x5402
%assign TCSETSW         0x5403
%assign TCSETSF         0x5404
%assign TCGETA          0x5405
%assign TCSETA          0x5406
%assign TCSETAW         0x5407
%assign TCSETAF         0x5408
%assign TCSBRK          0x5409
%assign TCXONC          0x540A
%assign TCFLSH          0x540B
%assign TIOCEXCL        0x540C
%assign TIOCNXCL        0x540D
%assign TIOCSCTTY       0x540E
%assign TIOCGPGRP       0x540F
%assign TIOCSPGRP       0x5410
%assign TIOCOUTQ        0x5411
%assign TIOCSTI         0x5412
%assign TIOCGWINSZ      0x5413
%assign TIOCSWINSZ      0x5414
%assign TIOCMGET        0x5415
%assign TIOCMBIS        0x5416
%assign TIOCMBIC        0x5417
%assign TIOCMSET        0x5418
%assign TIOCGSOFTCAR    0x5419
%assign TIOCSSOFTCAR    0x541A
%assign FIONREAD        0x541B
%assign TIOCINQ         FIONREAD
%assign TIOCLINUX       0x541C
%assign TIOCCONS        0x541D
%assign TIOCGSERIAL     0x541E
%assign TIOCSSERIAL     0x541F
%assign TIOCPKT         0x5420
%assign FIONBIO         0x5421
%assign TIOCNOTTY       0x5422
%assign TIOCSETD        0x5423
%assign TIOCGETD        0x5424
%assign TCSBRKP         0x5425  ;  Needed for POSIX tcsendbreak()
%assign TIOCTTYGSTRUCT  0x5426  ;  For debugging only
%assign TIOCSBRK        0x5427  ;  BSD compatibility
%assign TIOCCBRK        0x5428  ;  BSD compatibility
%assign TIOCGSID        0x5429  ;  Return the session ID of FD
;  Get Pty Number (of pty-mux device)
%assign TIOCGPTN        _IOR('T',0x30, 4)
;  Lock/unlock Pty
%assign TIOCSPTLCK      _IOW('T',0x31, 4)

%assign FIONCLEX        0x5450  ;  these numbers need to be adjusted.
%assign FIOCLEX         0x5451
%assign FIOASYNC        0x5452
%assign TIOCSERCONFIG   0x5453
%assign TIOCSERGWILD    0x5454
%assign TIOCSERSWILD    0x5455
%assign TIOCGLCKTRMIOS  0x5456
%assign TIOCSLCKTRMIOS  0x5457
%assign TIOCSERGSTRUCT  0x5458  ;  For debugging only
%assign TIOCSERGETLSR   0x5459  ;  Get line status register
%assign TIOCSERGETMULTI 0x545A  ;  Get multiport config
%assign TIOCSERSETMULTI 0x545B  ;  Set multiport config

%assign TIOCMIWAIT      0x545C  ;  wait for a change on serial input line(s)
%assign TIOCGICOUNT     0x545D  ;  read serial port inline interrupt counts
%assign TIOCGHAYESESP   0x545E  ;  Get Hayes ESP configuration
%assign TIOCSHAYESESP   0x545F  ;  Set Hayes ESP configuration

;  Used for packet mode
%assign TIOCPKT_DATA             0
%assign TIOCPKT_FLUSHREAD        1
%assign TIOCPKT_FLUSHWRITE       2
%assign TIOCPKT_STOP             4
%assign TIOCPKT_START            8
%assign TIOCPKT_NOSTOP          16
%assign TIOCPKT_DOSTOP          32

%assign TIOCSER_TEMT    0x01    ;  Transmitter physically empty

%endif

今回は termios.inc のうちのほんの一部分しか使っていません.

つぎに termios.inc を使って行編集機能付きの 1 行入力ルーチンを作成 します.1 行のバイト数を eax に設定して,入力行の保存領域の先頭アドレス を ebx に設定して call readline のように呼び出します.入力されたバイト数が eax に返ります.端末のローカルエコーが OFF の状態で呼び出します. VT100系の端末(Linux端末,xterm, kterm 等) で実行できます. 入力行の保存領域の先頭アドレスを eax, 保存領域のサイズを ebx にセット して呼び出してください.入力バイト数が eax に返ります.

入力された文字を格納するバッファの内容と画面に表示されている文字列を 一致するような動作をするだけのサブルーチンです.

Linux のコンソール画面では VT102と同じ (ほとんど?) エスケープ シーケンスを使用します.今回の例では以下のコードを使用します.

カーソル位置の保存 ^[7
カーソル位置の復帰 ^[8
カーソル左移動 ^B
カーソル位置の削除と右側を移動 ^[[1P
カーソル右左移動 ^[[1C
カーソル位置から行末までの消去 ^[[0K

Linux のコンソールには非常に多く機能を実現するシーケンスがあります. 自分で実際に試したこともなく,すべて解説する根性も無いので書籍を 紹介しておきます. 「Linux プログラミング アスキー出版局 ISBN4-7561-2044-X」の 20 章 にLinux のコンソール機能に関する詳しい記述があります. カーネルソースの drivers/char/console.c を読むという方法もあります :-)

READ_LINE では入力行は次に示すキーで編集できます.bash の行入力と同じです.

1 文字削除 ^D
バックスペース ^H
カーソル右移動 ^F →
カーソル左移動 ^B ←
;---------------------------------------------------------------------
; file          : readline.inc
; created       : 2000/08/20
; description   : read a line with editting
;
;         Copyright (C) 2000 Jun Mizutani <[email protected]>
;
; enter    eax : BufferSize
;          ebx : Buffer Address
; return   eax : no. of char
;---------------------------------------------------------------------

%ifndef __READLINE_INC
%define __READLINE_INC

%include "stdio.inc"
%include "termios.inc"

READ_LINE:
                push   esi
                push   edi
                push   ebx
                push   ecx
                push   edx
                mov    edi, ebx          ; Input Buffer
                mov    edx, eax          ; BufferSize
                xor    ecx, ecx
                mov    esi, ecx          ; current position
    .next_char:
                call   InChar
                cmp    al, 0x1B          ; ESC ?
                jnz    .bs
                call   translate_key_seq

    .bs:        cmp    al, 0x08          ; BS ?
                jnz    .delete
                test   esi, esi          ; if cp = 0 then next_char
                jz     .next_char
                dec    esi
                mov    ebx, CURSOR_LEFT
                call   OutPString
                jmp    short .del1

    .delete:
                cmp    al, 0x04          ; ^D ?
                jnz    .cursor_left
    .del1
                cmp    esi, ecx          ; if cp < eol then del2
                jb     .del2
                jmp    short .next_char
    .del2
                dec    ecx
                push   esi               ; p = cp
    .del3       cmp    esi, ecx          ; while(p<eol){buf[p]=buf[p+1]; p++}
                je     .del4
                mov    al, [edi+esi+1]
                mov    [edi + esi], al
                inc    esi
                jmp    short .del3
    .del4
                pop    esi
                mov    ebx, DEL_AT_CURSOR
                call   OutPString
                jmp    short .next_char

    .cursor_left:
                cmp    al, 0x02          ; ^B
                jnz    .cursor_right
                test   esi, esi          ; if cp = 0 then next_char
                jz     .next_char
                dec    esi
                mov    ebx, CURSOR_LEFT
                call   OutPString
                jmp    short .next_char

    .cursor_right:
                cmp    al, 0x06          ; ^F
                jnz    .enter_key
                cmp    ecx, esi          ; if cp = eol then next_char
                je     .next_char
                inc    esi
                mov    ebx, CURSOR_RIGHT
                call   OutPString
                jmp    .next_char

    .enter_key:
                cmp    al, 0x0A          ; enter ?
                jz     .in_exit
                cmp    al, 0x20
                jb     near .next_char   ; illegal chars

    .in_printable:
                inc    ecx
                inc    esi
                cmp    ecx, edx          ; buffer size
                jae    .in_toolong
                cmp    esi, ecx
                jb     .insert
                call   OutChar
                mov    [edi + esi-1], al
                jmp    .next_char
    .insert
                call   OutChar
                push   eax
                push   ecx               ; p = eol
                dec    ecx
    .ins1       cmp    esi, ecx          ; while(p=>cp){buf[p]=buf[p-1]; p--}
                ja     .ins2
                mov    al, [edi + ecx - 1]
                mov    [edi + ecx], al
                dec    ecx
                jmp    short .ins1
    .ins2
                pop    ecx
                pop    eax
                mov    [edi + esi - 1] ,al
                call   print_line_after_cp
                jmp    .next_char

    .in_toolong:
                dec    ecx
                dec    esi
                jmp    .next_char

    .in_exit:
                mov    byte [edi + ecx], 0
                call   NewLine
                mov    eax, ecx
                pop    edx
                pop    ecx
                pop    ebx
                pop    edi
                pop    esi
                ret

    print_line_after_cp:
                push   eax
                push   edx
                mov    ebx, SAVE_CURSOR
                call   OutPString
                mov    ebx, CLEAR_EOL
                call   OutPString
                lea    eax, [esi+edi]
                mov    edx, ecx
                sub    edx, esi
                call   OutString
                mov    ebx, RESTORE_CURSOR
                call   OutPString
                pop    edx
                pop    eax
                ret

    translate_key_seq:
                call   InChar
                cmp    al, '['
                jnz    .exit
                call   InChar
    .tk0:       cmp    al, 'A'
                jnz    .tk1
                mov    al, 'P' - 0x40  ; ^P
                ret
    .tk1:       cmp    al, 'B'
                jnz    .tk2
                mov    al, 'N' - 0x40  ; ^N
                ret
    .tk2:       cmp    al, 'C'
                jnz    .tk3
                mov    al, 'F' - 0x40  ; ^F
                ret
    .tk3:       cmp    al, 'D'
                jnz    .exit
                mov    al, 'B' - 0x40  ; ^B
                ret
    .exit       mov    al, 0
                ret

SAVE_CURSOR     db     2, 0x1B, '7'             ; ^[7
RESTORE_CURSOR  db     2, 0x1B, '8'             ; ^[8
DEL_AT_CURSOR   db     4, 0x1B, "[1P"           ; ^[[1P
CURSOR_RIGHT    db     4, 0x1B, "[1C"           ; ^[[1C
CURSOR_LEFT     db     4, 0x1B, "[1D"           ; ^[[1D
CLEAR_EOL       db     4, 0x1B, "[0K"           ; ^[[0K

%endif

termios.inc と readline.inc を使った簡単な例を作成します.行編集機能 を試すことができます.まずデフォルトの termios の c_cc 配列と変更後の c_cc 配列を表示します. 次に端末の設定を変更して入力バッファの内容が 画面に反映される状態で READ_LINE を呼び出し,その後入力結果を表示します. 次にウィンドウサイズを表示して終了します.

プログラムの全体の流れは以下のようになります.

  1. 現在の端末設定を保存
  2. 端末を新しく設定
  3. 編集機能付き 1 行入力
  4. 入力された行の内容を表示
  5. 端末の設定を復帰
  6. ウィンドウサイズを表示
  7. プログラム終了
;---------------------------------------------------------------------
;   2000/08/21
;
; nasm -f elf testterm.asm
; ld -s -o testterm testterm.o
;---------------------------------------------------------------------

section .text
global _start

%include "readline.inc"

;====================================
; メインルーチン
;====================================
_start:
                ; 端末のローカルエコーを OFF
                ; termios の保存と設定
                call   GET_TERMIOS
                call   SET_TERMIOS
                ; 新旧の termios の c_cc 配列を表示
                call   PRINT_TERMIOS
                call   NewLine

                ; 1 行入力
                mov    eax, MSG_EDIT
                call   OutAsciiZ
                mov    eax, 0x100
                mov    ebx, INBUFFER
                call   READ_LINE

                ; 入力内容の表示
                mov    edx, eax
                mov    eax, INBUFFER
                call   OutString
                call   NewLine

                ; termios の復帰
                call   RESTORE_TERMIOS

                ; ウィンドウサイズの表示
                call   NewLine
                mov    eax, MSG_WINSIZE
                call   OutAsciiZ
                call   PRINT_WINSIZE
                call   NewLine

                ; プログラム終了
                call   Exit

;------------------------------------
; ウィンドウサイズの表示
PRINT_WINSIZE:
                pusha
                call   tcgetwinsize
                mov    ebx, wsize
                xor    eax, eax
                mov    ax, [ebx + winsize.ws_col]
                call   PrintLeft
                mov    eax, "x"
                call   OutChar
                xor    eax, eax
                mov    ax, [ebx + winsize.ws_row]
                call   PrintLeft
                call   NewLine
                popa
                ret

;------------------------------------
; ウィンドウサイズの設定
SET_WINSIZE:
                pusha
                mov    ebx, wsize
                mov    ax, 80
                mov    [ebx + winsize.ws_col], ax
                mov    ax, 25
                mov    [ebx + winsize.ws_row], ax
                call   tcsetwinsize
                popa
                ret

;------------------------------------
; termios の表示
PRINT_TERMIOS:
                pusha
                mov    eax, MSG_C_CC
                call   OutAsciiZ
                mov    ecx,  new_termios - old_termios
                mov    esi,  old_termios
 .loop1:        mov    al,   [esi]
                call   PrintHex2
                inc    esi
                loop   .loop1
                call   NewLine
                mov    ecx,  new_termios_end - new_termios
                mov    esi,  new_termios
 .loop2:        mov    al,   [esi]
                call   PrintHex2
                inc    esi
                loop   .loop2
                call   NewLine
                popa
                ret

;------------------------------------
; 現在の termios を保存
GET_TERMIOS:
                pusha
                mov    ebx, old_termios
                call   tcgetattr
                mov    ecx,  new_termios - old_termios
                mov    esi,  old_termios
                mov    edi,  new_termios
                rep
                movsb
                popa
                ret

;------------------------------------
; 新しい termios を設定
; Rawモード, ECHO 無し, ECHONL 無し
; VTIME=0, VMIN=1 : 1バイト読み取られるまで待機
SET_TERMIOS:
                pusha
                mov    eax, [new_termios.c_lflag]
                and    eax, ~ICANON & ~ECHO & ~ECHONL & ~ISIG
                mov    [new_termios.c_lflag], eax
                mov    eax, 1
                mov    [new_termios.c_cc + VMIN], al
                mov    eax, 0
                mov    [new_termios.c_cc + VTIME], al
                mov    ebx, new_termios
                call   tcsetattr
                popa
                ret

;------------------------------------
; 保存されていた termios を復帰
RESTORE_TERMIOS:
                pusha
                mov    ebx, old_termios
                call   tcsetattr
                popa
                ret

;------------------------------------
;ウィンドウサイズの取得と設定
; tcgetwinsize(&ws)
; tcsetwinsize(&ws)
; eax, ebx, ecx, edx : destroyed
tcgetwinsize:
                mov    ecx,  TIOCGWINSZ
                jmp    short IOCTLWINSZ

tcsetwinsize:
                mov    ecx,  TIOCSWINSZ
IOCTLWINSZ:
                mov    edx, wsize
                mov    eax, SYS_ioctl ; (54)
                mov    ebx, 0         ; to stdout
                int    0x80           ; call kernel
                ret

;------------------------------------
; 標準入力の termios の取得と設定
; tcgetattr(&termios)
; tcsetattr(&termios)
; eax : destroyed
; ebx : termios buffer adress
; ecx, edx : destroyed
tcgetattr:
                mov    eax, TCGETS
                jmp    short IOCTL

tcsetattr:
                mov    eax, TCSETS

;------------------------------------
; 標準入力の ioctl の実行
; sys_ioctl(unsigned int fd, unsigned int cmd,
;           unsigned long arg)
; eax : cmd
; ebx : buffer adress
IOCTL:
                mov    ecx, eax       ; set cmd
                mov    edx, ebx       ; set arg
                mov    eax, 54        ; sys_ioctl
                mov    ebx, 0         ; to stdin
                int    0x80           ; call kernel
                ret

MSG_EDIT    db  'Input a line (Del:^D, BS:^H, Back:^B, Forward:^F).', 0x0A, 0
MSG_C_CC    db  'Termios c_cc[] : ', 0x0A, 0
MSG_WINSIZE db  'Window size : ', 0

;------------------------------------
section .bss

INBUFFER        resb 0x100

old_termios     istruc termios
                  .c_iflag    UINT   1       ; input mode flags
                  .c_oflag    UINT   1       ; output mode flags
                  .c_cflag    UINT   1       ; control mode flags
                  .c_lflag    UINT   1       ; local mode flags
                  .c_line     UCHAR  1       ; line discipline
                  .c_cc       UCHAR  NCCS    ; control characters
                iend

new_termios     istruc termios
                  .c_iflag    UINT   1       ; input mode flags
                  .c_oflag    UINT   1       ; output mode flags
                  .c_cflag    UINT   1       ; control mode flags
                  .c_lflag    UINT   1       ; local mode flags
                  .c_line     UCHAR  1       ; line discipline
                  .c_cc       UCHAR  NCCS    ; control characters
                iend
new_termios_end

wsize           istruc winsize
                  .ws_row     USHORT 1
                  .ws_col     USHORT 1
                  .ws_xpixel  USHORT 1
                  .ws_ypixel  USHORT 1
                iend

;------------------------------------

実行例

jm:~/ex_asm$ asm testterm
jm:~/ex_asm$ ./testterm
Termios c_cc[] :
0005000005000000BD0000003B8A000000031C7F150400010011131A00120F1716000000
0005000005000000BD000000318A000000031C7F150400010011131A00120F1716000000

Input a line (Del:^D, BS:^H, Back:^B, Forward:^F).
abcdefghij123klmnopqrstuvwxyzABCDEFGH4567IJKLMNOPQRST890UVWXYZ
abcdefghij123klmnopqrstuvwxyzABCDEFGH4567IJKLMNOPQRST890UVWXYZ

Window size : 80x40

testterm では日本語の入力はまったく考えていませんが,例えば Windows から telnet で起動すれば,日本語の入力も制限付きで可能です.

初期の MS-DOSプログラマはテキスト画面出力におけるハードウェアの違い を吸収するため,特定の文字コードが特殊な画面操作を行うように定義 されたエスケープシーケンスを使っていました. 場合によっては画面制御に使うエスケープシーケンスの方言を吸収する ために,さらに上位の抽象化されたコードを使う場合もありました. 例えば TurboPascal 3.0 のエディタでは非常に多くのハードウェアの差を 吸収するための機能があり,当時の日本で販売されていた種々のDOSマシン でも若干のコツが必要でしたが,英語版でも内蔵のエディタで日本語の テキストを使うことができました.その後,特定のハードウェアのシェアが 圧倒的になるにつれて, その特定のハードウェアの構成を前提とした ソフトウェアしか存在できなくなりました.その当時,特定のハードウェア の構成を前提とすることで,メモリに文字を書き込めば,即画面に反映する ことができました.そして画面上の好きな位置に好きな文字を高速に表示 することができるビデオメモリへの直接アクセスのメリットは,マイナーな 機種までもサポートするメリットより重要となってしまいました.

最近になって,ネットワークを経由して端末を制御する機会が増えて, ビデオメモリへの直接アクセスに比べて速度的に劣るシリアル端末を想定し た昔の方法が再び重要になってきました.ネットワーク経由でも動作する スクリーンエディタ (Linuxでは当たり前) を作成する時には面倒でも エスケープシーケンスを利用することになりますが,通信速度が高速化 されてきたため,画面更新の速度はエスケープシーケンスを使った場合でも 問題になりません.

とにかく,画面を思い通りに操るエスケープシーケンスを考えるのも 面白いものです.