Editor自作
これは後期に進めることを予定した資料であり、現在制作中です (3^5)
https://gyazo.com/dc5203245325139b281b25bb0607756a
関連
ヘッダファイルに関しては知らないものが出てきたら各自調べてください。特別必要であると判断したものは記述しています。コードを書いた順に記載しているのでいい感じにファイルを分けるか並び替えるかしてください。
1. フローの理解
まずはファイルを読み込んでその内容を表示させるプログラムから書いていきましょう。
前期の最後に作成したcatと異なるのはエスケープシーケンスを用いて画面制御を行う点です。
エスケープシーケンスの中でもCSIシーケンスと呼ばれるものを用いると、画面上のカーソル位置や文字色・背景色・明るさ・下線・点滅・反転などのテキスト属性を自由に設定できます。
CSIシーケンスは\e, \x1b, \033などから始まる。(どれも同じ意味)
ここまでのコードはこんな感じ(まだ簡単)
code:kilo.c
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: kilo <filename>\n");
return 1;
}
FILE *f = fopen(argv1, "r"); fread(buf, sizeof(char), BUFSIZE, f);
printf("\e[2J"); //clear
printf("\e[1;1H"); //move cursor to 1,1 printf("\e[?25l"); //hide cursor
puts(buf);
printf("\e[?25h"); //print cursor
return 0;
}
基本的な流れ
入力を受け取る->表示文字列(構造体)を変更する->出力する->入力待ち、という感じ
どちらかというとオブジェクト指向が得意な記述パターンっぽい
おあそび
引数で指定したファイルを出力するが数値を入力するとファイルの最初をその数値に書き換えて表示するよく分からんエディタ
code:onTheWay.c
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: kilo <filename>\n");
return 1;
}
FILE *f = fopen(argv1, "r"); fread(buf, sizeof(char), BUFSIZE, f);
while(1) {
int input = 0;
scanf("%d", &input);
printf("\e[2J"); //clear
printf("\e[1;1H"); //move cursor to 1,1 printf("\e[?25l"); //hide cursor
puts(buf);
printf("\e[1;1H");
printf("%d\n", input);
printf("\e[?25h"); //print cursor
}
return 0;
}
2. 基本的なフローを構築
どこまでが最低限必要かわからないがとりあえずstruct editorConfigを作成(1の内容は消していいです)
code:editorConfig.c
/* This structure represents a single line of the file we are editing. */
typedef struct erow {
int idx; /* Row index in the file, zero-based. */
int size; /* Size of the row, excluding the null term. */
char *chars; /* Row content. */
} erow;
struct editorConfig {
int cx,cy; /* Cursor x and y position in characters */
int rowoff; /* Offset of row displayed. */
int coloff; /* Offset of column displayed. */
int screenrows; /* Number of rows that we can show */
int screencols; /* Number of cols that we can show */
int numrows; /* Number of rows */
int rawmode; /* Is terminal raw mode enabled? */
erow *row; /* Rows */
char *filename; /* Currently open filename */
};
それぞれのメンバの詳細はコメントの記述のとおりである。
続いて、mainを書き換える。
code:main.c
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: kilo <filename>\n");
return 1;
}
initEditor();
enableRawMode(STDIN_FILENO);
while(1) {
editorRefreshScreen();
editorProcessKeypress(STDIN_FILENO);
}
return 0;
}
まず、initEditorから書いていく。initEditor関数はeditorConfig構造体のグローバル変数Eを初期化する関数で、ウィンドウのサイズに関わるメンバを設定する。
code:initEditor.c
int getWindowSize(int ifd, int ofd, int *rows, int *cols) {
struct winsize ws;
if(ioctl(1, TIOCGWINSZ, &ws) != -1 && ws.ws_col != 0) {
*cols = ws.ws_col;
*rows = ws.ws_row;
return 0;
}
return -1;
}
void initEditor(void) {
E.cx = 0;
E.cy = 0;
E.rowoff = 0;
E.coloff = 0;
E.numrows = 0;
E.row = NULL;
E.filename = NULL;
if (getWindowSize(STDIN_FILENO,STDOUT_FILENO,
&E.screenrows,&E.screencols) == -1)
{
perror("Unable to query the screen for size (columns / rows)");
exit(1);
}
}
getWindowSizeでは、sys/ioctl.hのioctlを用いてウィンドウの大きさを取得している。ioctlの引数1はstdoutのfdを表している。ioctl関数が正常に動作した場合は構造体のそれぞれのプロパティを設定している。ioctlのマニュアル 次に、editorOpenを書いていく。これは単純にファイルをオープンしてその中身をEに設定するというものだ。ENOENTはファイルが存在しないときのエラーであるから今回はエラー終了はせずそのままreturnする。
code:editorOpen.c
void editorInsertRow(int at, char *s, size_t len) {
if(at > E.numrows) return;
E.row = realloc(E.row, sizeof(erow)*(E.numrows+1));
if(at < E.numrows) {
memmove(E.row+at, E.row+at, sizeof(E.row0)*(E.numrows-at)); for(int i = at+1; i <= E.numrows; i++) E.rowi.idx++; }
E.rowat.chars = malloc(len + 1); memcpy(E.rowat.chars, s, len + 1); E.numrows++;
}
int editorOpen(char *filename) {
FILE *fp;
free(E.filename);
E.filename = strdup(filename);
fp = fopen(filename, "r");
if(!fp) {
if(errno != ENOENT) {
perror("Opening file");
exit(1);
}
return 1;
}
char *line = NULL;
size_t cap;
ssize_t len;
while(len = getline(&line, &cap, fp) != -1) {
editorInsertRow(E.numrows, line, len);
}
free(line);
fclose(fp);
return 0;
}
そして、editorRefreshScreenを書く。
abuf構造体はabAppend関数で必要なメモリをアロケートしながら文字を末尾に追加する。lenメンバの大きさ分しかbufの領域を持たないので省メモリである。
改行に/rが必要な理由は外して実行して見れば分かります…(abAppendに与えるlenも-1しておくこと)
code:editorRefreshScreen.c
struct abuf {
char *buf;
int len;
};
void abAppend(struct abuf *ab, const char *s, int len) {
char *new = realloc(ab->buf, ab->len + len);
if(new == NULL) return;
memcpy(new+ab->len, s, len);
ab->buf = new;
ab->len += len;
}
void abFree(struct abuf *ab) {
free(ab->buf);
}
void editorRefreshScreen(void) {
struct abuf ab = {NULL, 0};
abAppend(&ab, "\e[?25l", 6); //hide cursor
abAppend(&ab, "\e[H", 3); //go home
for(int y = 0; y < E.screenrows; y++) {
int filerow = E.rowoff + y;
if(filerow >= E.numrows) {
abAppend(&ab, "~\e[0K\r\n", 7);
continue;
}
int len = r->size - E.coloff;
if(len > 0) {
if(len > E.screencols) len = E.screencols;
char *c = r->chars + E.coloff;
for(int j = 0; j < len; j++) abAppend(&ab, c+j, 1);
}
abAppend(&ab, "\e[39m", 5);
abAppend(&ab, "\e[0K", 4);
abAppend(&ab, "\r\n", 2);
}
snprintf(buf, sizeof(buf), "\e[%d;%dH", E.cy+1, E.cx);
abAppend(&ab, buf, strlen(buf));
abAppend(&ab, "\e[?25h", 6);
write(STDOUT_FILENO, ab.buf, ab.len);
abFree(&ab);
}
ここまで書くと開いたファイルが表示されつつカーソルが左上にくるようになる。
しかしプロンプトを含めたそれまでの画面はクリアされないのでここでenableRawModeを書く。
code:enableRawMode.c
static struct termios orig_termios; /* In order to restore at exit.*/
void disableRawMode(int fd) {
/* Don't even check the return value as it's too late. */
if (E.rawmode) {
tcsetattr(fd,TCSAFLUSH,&orig_termios);
E.rawmode = 0;
}
}
/* Called at exit to avoid remaining in raw mode. */
void editorAtExit(void) {
disableRawMode(STDIN_FILENO);
}
/* Raw mode: 1960 magic shit. */
int enableRawMode(int fd) {
struct termios raw;
if (E.rawmode) return 0; /* Already enabled. */
if (!isatty(STDIN_FILENO)) goto fatal;
atexit(editorAtExit);
if (tcgetattr(fd,&orig_termios) == -1) goto fatal;
raw = orig_termios; /* modify the original mode */
/* input modes: no break, no CR to NL, no parity check, no strip char,
* no start/stop output control. */
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
/* output modes - disable post processing */
raw.c_oflag &= ~(OPOST);
/* control modes - set 8 bit chars */
raw.c_cflag |= (CS8);
/* local modes - choing off, canonical off, no extended functions,
* no signal chars (^Z,^C) */
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
/* control chars - set return condition: min number of bytes and timer. */
raw.c_ccVMIN = 0; /* Return each byte, or zero for timeout. */ raw.c_ccVTIME = 1; /* 100 ms timeout (unit is tens of second). */ /* put terminal in raw mode after flushing */
if (tcsetattr(fd,TCSAFLUSH,&raw) < 0) goto fatal;
E.rawmode = 1;
return 0;
fatal:
errno = ENOTTY;
return -1;
}
最後にeditorProcessKeyPressを実装すれば基本的なフローが完成する。
キー入力は1文字ではない場合がある(上記のエスケープシーケンスの記事参照)。ESCキーの後の任意の文字コードはエスケープシーケンスとして認識されるためである。
例えば、十字キー→は^[[Cと入力されている。^[がエスケープキーを表しているためこれはESC, [, Cの3文字から構成されていると言える。この関数ではキー入力を1文字分認識しその文字コードを返すが、ESCや十字キーなどの複数の文字コードから成る入力も検知する必要がある。
列挙型を用いて特殊なキー入力を表現しているが、1つの文字コードで表現されるものはデフォルト値をその文字コードの値として与えている。
文字コード表(縦と横の値の和がその文字を表す)
https://gyazo.com/f5ef1a95dcc196999fcebc7fcf59afc2
code:editorProcessKeyPress.c
enum KEY_ACTION{
KEY_NULL = 0, /* NULL */
CTRL_C = 3, /* Ctrl-c */
CTRL_D = 4, /* Ctrl-d */
CTRL_F = 6, /* Ctrl-f */
CTRL_H = 8, /* Ctrl-h */
TAB = 9, /* Tab */
CTRL_L = 12, /* Ctrl+l */
ENTER = 13, /* Enter */
CTRL_Q = 17, /* Ctrl-q */
CTRL_S = 19, /* Ctrl-s */
CTRL_U = 21, /* Ctrl-u */
ESC = 27, /* Escape */
BACKSPACE = 127, /* Backspace */
/* The following are just soft codes, not really reported by the
* terminal directly. */
ARROW_LEFT = 1000,
ARROW_RIGHT,
ARROW_UP,
ARROW_DOWN,
DEL_KEY,
HOME_KEY,
END_KEY,
PAGE_UP,
PAGE_DOWN
};
int editorReadKey(int fd) {
int nread;
while((nread = read(fd, &c, 1)) == 0);
if(nread == -1) exit(1);
while(1) {
switch(c) {
case ESC:
if(read(fd, seq, 1) == 0) return ESC;
if(read(fd, seq + 1, 1) == 0) return ESC;
if('0' <= seq1 && seq1 <= '9') { if(read(fd, seq+2, 1) == 0) return ESC;
case '3': return DEL_KEY;
case '5': return PAGE_UP;
case '6': return PAGE_DOWN;
}
}
} else {
case 'A': return ARROW_UP;
case 'B': return ARROW_DOWN;
case 'C': return ARROW_RIGHT;
case 'D': return ARROW_LEFT;
case 'H': return HOME_KEY;
case 'F': return END_KEY;
}
}
case 'H': return HOME_KEY;
case 'F': return END_KEY;
}
}
break;
default:
return c;
}
}
}
void editorProcessKeypress(int fd) {
int c = editorReadKey(fd);
printf("%d\n",c);
if(c == 'q') exit(0);
}
これでファイルを開き、qを入力すると終了するプログラムができた。