我尝试使用 ncurses 制作一个简单的文件查看器,允许用户滚动文件并在底部显示状态栏。
问题是
refresh()
(通过getch()
调用)太慢了。与 Vim 不同,滚动不平滑,偶尔会挂起和卡顿。
这是完整的源代码,使用
gcc main.c -lncurses
编译:
#include <ncurses.h>
#include <stdlib.h>
#include <stdio.h>
#include <locale.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#define MIN(a, b) ({ \
__auto_type _a = (a); \
__auto_type _b = (b); \
_a < _b ? _a : _b; \
})
struct line {
int indent;
chtype *text;
size_t ntext;
};
struct buffer {
struct line *lines;
size_t nlines;
size_t nbytes;
};
struct buffer buf;
WINDOW *view_win;
int view_width, view_height;
size_t v_scroll;
void buf_draw_lines(int y, size_t from, size_t to);
void update_size(void)
{
view_width = COLS;
view_height = MIN((size_t) (LINES - 1), buf.nlines);
if (view_win != NULL)
delwin(view_win);
view_win = newwin(view_height, view_width, 0, 0);
scrollok(view_win, true);
buf_draw_lines(0, 0, buf.nlines - 1);
/* Draw full status bar */
mvprintw(LINES - 1, 0,
"%4zu-%4zu/%4zu (%3d%%) %zub (view=%dx%d) (term=%dx%d)",
v_scroll + 1, v_scroll + view_height,
buf.nlines,
buf.nlines == (size_t) view_height ? 100 :
(int) (100 *
v_scroll / (buf.nlines - view_height)),
buf.nbytes,
view_width, view_height,
COLS, LINES);
hline(' ', COLS - getcurx(stdscr));
}
int line_append(struct line *line, char ch)
{
chtype *newtext;
if (line->ntext == 0 && isblank(ch)) {
line->indent += ch == ' ' ? 1 : 8;
return 0;
}
newtext = realloc(line->text, sizeof(*line->text) * (line->ntext + 1));
if (newtext == NULL)
return -1;
line->text = newtext;
line->text[line->ntext++] = ch;
return 0;
}
struct line *buf_newline(void)
{
struct line *newlines;
newlines = realloc(buf.lines, sizeof(*buf.lines) * (buf.nlines + 1));
if (newlines == NULL)
return NULL;
buf.lines = newlines;
newlines += buf.nlines;
memset(newlines, 0, sizeof(*newlines));
buf.nlines++;
return newlines;
}
int buf_read_file(const char *file)
{
int fd;
char b[1024];
struct line *line;
ssize_t r;
if ((fd = open(file, O_RDONLY)) < 0)
return -1;
buf.nlines = 0;
buf.nbytes = 0;
if ((line = buf_newline()) == NULL) {
close(fd);
return -1;
}
while ((r = read(fd, b, sizeof(b))) > 0)
for(const char *p = b; r; r--, p++) {
if (*p == '\n' && ((line = buf_newline()) == NULL))
goto sudden_out_of_mem;
if (*p != '\n' && line_append(line, *p) < 0)
goto sudden_out_of_mem;
buf.nbytes++;
}
/* fall through */
sudden_out_of_mem:
close(fd);
return 0;
}
void buf_draw_lines(int y, size_t from, size_t to)
{
if (to >= buf.nlines)
to = buf.nlines - 1;
if ((size_t) (view_height - y) <= to - from)
to = view_height - y + from - 1;
for (; from <= to; from++, y++) {
const struct line *const line = buf.lines + from;
mvwaddchnstr(view_win, y, line->indent,
line->text, line->ntext);
}
}
void handle_char(int c)
{
switch (c) {
case KEY_RESIZE:
update_size();
break;
case 'j':
case KEY_DOWN:
if (v_scroll == buf.nlines - view_height)
break;
v_scroll++;
wscrl(view_win, 1);
buf_draw_lines(view_height - 1,
v_scroll + view_height - 1,
v_scroll + view_height - 1);
break;
case KEY_UP:
case 'k':
if (v_scroll == 0)
break;
v_scroll--;
wscrl(view_win, -1);
buf_draw_lines(0, v_scroll, v_scroll);
break;
case KEY_HOME:
case 'g':
v_scroll = 0;
werase(view_win);
buf_draw_lines(0, 0, buf.nlines - 1);
break;
case KEY_END:
case 'G':
v_scroll = buf.nlines - view_height;
werase(view_win);
buf_draw_lines(0, buf.nlines - view_height, buf.nlines - 1);
break;
}
}
int main(int argc, char **argv)
{
if (argc < 2) {
fprintf(stderr, "usage: %s <file name>\n", argv[0]);
return -1;
}
setlocale(LC_ALL, "");
initscr();
noecho();
raw();
curs_set(0);
start_color();
keypad(stdscr, true);
scrollok(stdscr, true);
idlok(stdscr, true);
if (buf_read_file(argv[1]) < 0) {
endwin();
fprintf(stderr, "error reading file '%s'\n", argv[1]);
return -1;
}
refresh();
attr_set(A_REVERSE, 0, NULL);
update_size();
/* Main loop */
while (1) {
mvprintw(LINES - 1, 0, "%4zu-%4zu",
v_scroll + 1, v_scroll + view_height);
wrefresh(view_win);
const int c = getch();
if (c == 0x03 || c == 'q')
break;
handle_char(c);
}
for (size_t i = 0; i < buf.nlines; i++)
free(buf.lines[i].text);
free(buf.lines);
endwin();
return 0;
}
我不确定我的代码是否存在问题,或者 ncurses 是否只是很慢。删除
start_color()
显着提高了性能,并且缩小了终端尺寸。
我尝试按住
j
键并使用 xset r rate 200 50
,但程序无法跟上滚动。
我现在必须使用较低级别的诅咒例程来尝试以某种方式优化它还是我错过了一些东西?
编辑: 现在我尝试使用 termios 和 ansi 转义码创建一个新程序,但 Vim 中并没有明显的差异。但重新实现wheel并制作curses 2.0是我的计划。
空程序代码:
#include <ncurses.h>
#include <time.h>
int main(void)
{
int counter = 0;
clock_t start, end;
double diff;
initscr();
start_color();
noecho();
raw();
curs_set(0);
timeout(0);
printw("q - quit; r - reset");
start = clock();
while (1) {
const int c = getch();
if (c == 'q')
break;
switch (c) {
case 'r':
counter = 0;
break;
}
if (counter == 0)
start = clock();
end = clock();
diff = (double) (end - start) / CLOCKS_PER_SEC;
mvprintw(1, 0, "%d/%f %f refreshes per second\n",
counter, diff, (double) counter / diff);
counter++;
}
endwin();
end = clock();
diff = (double) (end - start) / CLOCKS_PER_SEC;
printf("%d/%f %f refreshes per second\n",
counter, diff, (double) counter / diff);
return 0;
}
完整代码:
#include <ncurses.h>
#include <stdlib.h>
#include <stdio.h>
#include <locale.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#define MIN(a, b) ({ \
__auto_type _a = (a); \
__auto_type _b = (b); \
_a < _b ? _a : _b; \
})
struct line {
int indent;
chtype *text;
size_t ntext;
};
struct buffer {
struct line *lines;
size_t nlines;
size_t nbytes;
};
struct buffer buf;
WINDOW *view_win;
int view_width, view_height;
size_t v_scroll;
void buf_draw_lines(int y, size_t from, size_t to);
void update_size(void)
{
view_width = COLS;
view_height = MIN((size_t) (LINES - 2), buf.nlines);
if (view_win != NULL)
delwin(view_win);
view_win = newwin(view_height, view_width, 1, 0);
scrollok(view_win, true);
buf_draw_lines(0, 0, buf.nlines - 1);
/* Draw full status bar */
mvprintw(LINES - 1, 0,
"%4zu-%4zu/%4zu (%3d%%) %zub (view=%dx%d) (term=%dx%d)",
v_scroll + 1, v_scroll + view_height,
buf.nlines,
buf.nlines == (size_t) view_height ? 100 :
(int) (100 *
v_scroll / (buf.nlines - view_height)),
buf.nbytes,
view_width, view_height,
COLS, LINES);
hline(' ', COLS - getcurx(stdscr));
}
int line_append(struct line *line, char ch)
{
chtype *newtext;
if (line->ntext == 0 && isblank(ch)) {
line->indent += ch == ' ' ? 1 : 8;
return 0;
}
newtext = realloc(line->text, sizeof(*line->text) * (line->ntext + 1));
if (newtext == NULL)
return -1;
line->text = newtext;
line->text[line->ntext++] = ch;
return 0;
}
struct line *buf_newline(void)
{
struct line *newlines;
newlines = realloc(buf.lines, sizeof(*buf.lines) * (buf.nlines + 1));
if (newlines == NULL)
return NULL;
buf.lines = newlines;
newlines += buf.nlines;
memset(newlines, 0, sizeof(*newlines));
buf.nlines++;
return newlines;
}
int buf_read_file(const char *file)
{
int fd;
char b[1024];
struct line *line;
ssize_t r;
if ((fd = open(file, O_RDONLY)) < 0)
return -1;
buf.nlines = 0;
buf.nbytes = 0;
if ((line = buf_newline()) == NULL) {
close(fd);
return -1;
}
while ((r = read(fd, b, sizeof(b))) > 0)
for(const char *p = b; r; r--, p++) {
if (*p == '\n' && ((line = buf_newline()) == NULL))
goto sudden_out_of_mem;
if (*p != '\n' && line_append(line, *p) < 0)
goto sudden_out_of_mem;
buf.nbytes++;
}
/* fall through */
sudden_out_of_mem:
close(fd);
return 0;
}
void buf_draw_lines(int y, size_t from, size_t to)
{
if (to >= buf.nlines)
to = buf.nlines - 1;
if ((size_t) (view_height - y) <= to - from)
to = view_height - y + from - 1;
for (; from <= to; from++, y++) {
const struct line *const line = buf.lines + from;
mvwaddchnstr(view_win, y, line->indent,
line->text, line->ntext);
}
}
void handle_char(int c)
{
switch (c) {
case KEY_RESIZE:
update_size();
break;
case 'j':
case KEY_DOWN:
if (v_scroll == buf.nlines - view_height)
break;
v_scroll++;
wscrl(view_win, 1);
buf_draw_lines(view_height - 1,
v_scroll + view_height - 1,
v_scroll + view_height - 1);
break;
case KEY_UP:
case 'k':
if (v_scroll == 0)
break;
v_scroll--;
wscrl(view_win, -1);
buf_draw_lines(0, v_scroll, v_scroll);
break;
case KEY_HOME:
case 'g':
v_scroll = 0;
werase(view_win);
buf_draw_lines(0, 0, buf.nlines - 1);
break;
case KEY_END:
case 'G':
v_scroll = buf.nlines - view_height;
werase(view_win);
buf_draw_lines(0, buf.nlines - view_height, buf.nlines - 1);
break;
}
}
int main(int argc, char **argv)
{
int counter = 0;
clock_t start, end;
double diff;
if (argc < 2) {
fprintf(stderr, "usage: %s <file name>\n", argv[0]);
return -1;
}
setlocale(LC_ALL, "");
initscr();
noecho();
raw();
curs_set(0);
start_color();
keypad(stdscr, true);
scrollok(stdscr, true);
idlok(stdscr, true);
if (buf_read_file(argv[1]) < 0) {
endwin();
fprintf(stderr, "error reading file '%s'\n", argv[1]);
return -1;
}
timeout(0);
refresh();
attr_set(A_REVERSE, 0, NULL);
update_size();
/* Main loop */
while (1) {
mvprintw(LINES - 1, 0, "%4zu-%4zu",
v_scroll + 1, v_scroll + view_height);
wrefresh(view_win);
const int c = getch();
if (c == 0x03 || c == 'q')
break;
if (c == 'r')
counter = 0;
if (counter == 0)
start = clock();
end = clock();
diff = (double) (end - start) / CLOCKS_PER_SEC;
mvprintw(0, 0, "%d/%f %f refreshes per second\n",
counter, diff, (double) counter / diff);
clrtoeol();
counter++;
handle_char(c);
}
for (size_t i = 0; i < buf.nlines; i++)
free(buf.lines[i].text);
free(buf.lines);
endwin();
end = clock();
diff = (double) (end - start) / CLOCKS_PER_SEC;
printf("%d/%f %f refreshes per second\n",
counter, diff, (double) counter / diff);
return 0;
}
报告空程序:
92928/10.114075 9187.988027 refreshes per second
完整节目报道:
128582/10.249236 12545.520466 refreshes per second
我很惊讶地看到完整的程序完成了更多的刷新。不确定 ncurses 在那里做什么。
抱歉,我没有看到你的程序运行缓慢...我已经以 1200 波特的速度对其进行了测试,并且刷新工作正常。
您可能对curses库的设计有一些误解。它旨在优化发送到终端的输出量,因为在过去的
vi
(不是 vim)tty 线路连接到低波特率,因此传输每个字符的时间非常重要。
我提到vi(而不是vim)是因为vim不是良好curses行为的一个很好的例子,因为它反复绘制和重绘屏幕的许多部分(可能更优先考虑计算量,而不是重绘某些部分的可能性)多次)如果您将低速率下的屏幕重绘与低波特率下的 vi(或新的 nvi)程序进行比较,您会发现很多差异。
我看到你的程序以正确的方式重绘(正如 nvi 所做的那样),这最大限度地减少了绘制的字符数。如果您以 1200 波特或更低的速度在真正的串行线路上运行,您将看到实际的差异,并且您会看到 vim 由于重绘量而崩溃。为了测试我使用 y 编写的小程序以 tty 线路上固定的速度运行普通的 xterm 终端(通过
stty
命令),并将其发布在 Github 中。尝试一下并将线路设置为 1200 波特,您将看到两个程序都在运行,您的程序将成为获胜者。 :)
你的程序有点晦涩,有点难以阅读,但是你已经做得很好了。看起来效果不错。
注意:我观察到(在重新调整终端窗口大小时,因为拖动指针时它会将大量调整大小操作排入队列)您将所有刷新排队并按顺序执行它们。您应该检查是否仍有输入需要处理并处理所有内容(curses 在基于 ram 的屏幕中处理它),并避免为每个微小的修改调用刷新。如果你能批量处理所有输入,然后调用刷新,你将节省大量更新。这是在 vi (nvi) 中完成的,但不是在 vim 中完成的,vim 中的行为与您所说的相同。刷新的目的正是为了让您能够不同步屏幕更新与必须在屏幕上完成的操作量。