blob: 836096059387365ee5c12e378bed8e2addea67cf [file] [log] [blame]
// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <assert.h>
#include <fcntl.h>
#include <lib/gfx-font-data/gfx-font-data.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <zircon/process.h>
#include <zircon/syscalls.h>
#include <fbl/auto_lock.h>
#include "vc.h"
// Default height/width (in px) of console before any displays are
// attached, since we need somewhere to put any data that is received.
#define DEFAULT_WIDTH 1024
#define DEFAULT_HEIGHT 768
#define SCROLLBACK_ROWS 1024 // TODO make configurable
#define ABS(val) (((val) >= 0) ? (val) : -(val))
const gfx_font* g_font = &gfx_font_9x16;
const keychar_t* g_keymap = qwerty_map;
static zx_status_t vc_setup(vc_t* vc, const color_scheme_t* color_scheme) {
// calculate how many rows/columns we have
vc->rows = DEFAULT_HEIGHT / vc->charh;
vc->columns = DEFAULT_WIDTH / vc->charw;
vc->scrollback_rows_max = SCROLLBACK_ROWS;
vc->scrollback_rows_count = 0;
vc->scrollback_offset = 0;
// allocate the text buffer
vc->text_buf =
reinterpret_cast<vc_char_t*>(calloc(1, vc->rows * vc->columns * sizeof(vc_char_t)));
if (!vc->text_buf)
return ZX_ERR_NO_MEMORY;
// allocate the scrollback buffer
vc->scrollback_buf = reinterpret_cast<vc_char_t*>(
calloc(1, vc->scrollback_rows_max * vc->columns * sizeof(vc_char_t)));
if (!vc->scrollback_buf) {
free(vc->text_buf);
return ZX_ERR_NO_MEMORY;
}
// set up the default palette
memcpy(&vc->palette, default_palette, sizeof(default_palette));
vc->front_color = color_scheme->front;
vc->back_color = color_scheme->back;
return ZX_OK;
}
bool vc_graphics_enabled(vc_t* vc) { return vc->graphics && vc->graphics->vc_gfx; }
static void vc_invalidate(void* cookie, int x0, int y0, int w, int h) {
vc_t* vc = reinterpret_cast<vc_t*>(cookie);
if (!g_vc_owns_display || !vc->active || !vc_graphics_enabled(vc)) {
return;
}
assert(h >= 0);
int y1 = y0 + h;
assert(y0 <= static_cast<int>(vc->rows));
assert(y1 <= static_cast<int>(vc->rows));
// Clip the y range so that we don't unnecessarily draw characters
// outside the visible range, and so that we don't draw characters into
// the bottom margin.
int visible_y0 = vc->viewport_y;
int visible_y1 = vc->viewport_y + vc_rows(vc);
y0 = MAX(y0, visible_y0);
y1 = MIN(y1, visible_y1);
for (int y = y0; y < y1; y++) {
if (y < 0) {
// Scrollback row.
vc_char_t* row = vc_get_scrollback_line_ptr(vc, y + vc->scrollback_rows_count);
for (int x = x0; x < x0 + w; x++) {
vc_gfx_draw_char(vc->graphics, vc, row[x], x, y - vc->viewport_y,
/* invert= */ false);
}
} else {
// Row in the main console region (non-scrollback).
vc_char_t* row = &vc->text_buf[y * vc->columns];
for (int x = x0; x < x0 + w; x++) {
// Check whether we should display the cursor at this
// position. Note that it's possible that the cursor is
// outside the display area (vc->cursor_x ==
// vc->columns). In that case, we won't display the
// cursor, even if there's a margin. This matches
// gnome-terminal.
bool invert = (!vc->hide_cursor && static_cast<unsigned>(x) == vc->cursor_x &&
static_cast<unsigned>(y) == vc->cursor_y);
vc_gfx_draw_char(vc->graphics, vc, row[x], x, y - vc->viewport_y, invert);
}
}
}
}
// implement tc callbacks:
static inline void vc_invalidate_lines(vc_t* vc, int y, int h) {
if (y < vc->invy0) {
vc->invy0 = y;
}
y += h;
if (y > vc->invy1) {
vc->invy1 = y;
}
}
static void vc_tc_invalidate(void* cookie, int x0, int y0, int w, int h) {
vc_t* vc = reinterpret_cast<vc_t*>(cookie);
vc_invalidate(cookie, x0, y0, w, h);
vc_invalidate_lines(vc, y0, h);
}
static void vc_tc_movecursor(void* cookie, int x, int y) {
vc_t* vc = reinterpret_cast<vc_t*>(cookie);
unsigned old_x = vc->cursor_x;
unsigned old_y = vc->cursor_y;
vc->cursor_x = x;
vc->cursor_y = y;
if (g_vc_owns_display && vc->active && !vc->hide_cursor) {
// Clear the cursor from its old position.
vc_invalidate(cookie, old_x, old_y, 1, 1);
vc_invalidate_lines(vc, old_y, 1);
// Display the cursor in its new position.
vc_invalidate(cookie, vc->cursor_x, vc->cursor_y, 1, 1);
vc_invalidate_lines(vc, vc->cursor_y, 1);
}
}
static void vc_tc_scrollback_buffer_push(vc_t* vc, vc_char_t* src) {
unsigned dest_row;
assert(vc->scrollback_rows_count <= vc->scrollback_rows_max);
if (vc->scrollback_rows_count < vc->scrollback_rows_max) {
// Add a row without dropping any existing rows.
assert(vc->scrollback_offset == 0);
dest_row = vc->scrollback_rows_count++;
} else {
// Add a row and drop an existing row.
assert(vc->scrollback_offset < vc->scrollback_rows_max);
dest_row = vc->scrollback_offset++;
if (vc->scrollback_offset == vc->scrollback_rows_max)
vc->scrollback_offset = 0;
}
vc_char_t* dst = &vc->scrollback_buf[dest_row * vc->columns];
memcpy(dst, src, vc->columns * sizeof(vc_char_t));
}
static void vc_tc_push_scrollback_line(void* cookie, int y) {
vc_t* vc = reinterpret_cast<vc_t*>(cookie);
vc_char_t* src = &vc->text_buf[y * vc->columns];
vc_tc_scrollback_buffer_push(vc, src);
// If we're displaying only the main console region (and no
// scrollback), then keep displaying that (i.e. don't modify
// viewport_y).
if (vc->viewport_y < 0) {
// We are displaying some of the scrollback buffer.
if (vc->viewport_y > -static_cast<int>(vc->scrollback_rows_max)) {
// Scroll the viewport to continue displaying the same point in
// the scrollback buffer.
--vc->viewport_y;
} else {
// We were displaying the line at the top of the scrollback
// buffer, but we dropped that line from the buffer. We could
// leave the display as it was (which is what gnome-terminal
// does) and not scroll the display. However, that causes
// problems. If the user later scrolls down, we won't
// necessarily be able to display the lines below -- we might
// have dropped those too. So, instead, let's scroll the
// display and remove the scrollback line that was lost.
//
// For simplicity, fall back to redrawing everything.
vc_invalidate(vc, 0, -vc->scrollback_rows_max, vc->columns, vc_rows(vc));
vc_render(vc);
}
}
}
static void vc_set_cursor_hidden(vc_t* vc, bool hide) {
if (vc->hide_cursor == hide)
return;
vc->hide_cursor = hide;
if (g_vc_owns_display && vc->active) {
vc_invalidate(vc, vc->cursor_x, vc->cursor_y, 1, 1);
vc_invalidate_lines(vc, vc->cursor_y, 1);
}
}
static void vc_tc_copy_lines(void* cookie, int y_dest, int y_src, int line_count) {
vc_t* vc = reinterpret_cast<vc_t*>(cookie);
if (vc->viewport_y < 0) {
tc_copy_lines(&vc->textcon, y_dest, y_src, line_count);
// The viewport is scrolled. For simplicity, fall back to
// redrawing all of the non-scrollback lines in this case.
int rows = vc_rows(vc);
vc_invalidate(vc, 0, 0, vc->columns, rows);
vc_invalidate_lines(vc, 0, rows);
return;
}
// Remove the cursor from the display before copying the lines on
// screen, otherwise we might be copying a rendering of the cursor to a
// position where the cursor isn't. This must be done before the
// tc_copy_lines() call, otherwise we might render the wrong character.
bool old_hide_cursor = vc->hide_cursor;
if (g_vc_owns_display && vc->active) {
vc_set_cursor_hidden(vc, true);
}
// The next two calls can be done in any order.
tc_copy_lines(&vc->textcon, y_dest, y_src, line_count);
if (g_vc_owns_display && vc->active && vc_graphics_enabled(vc)) {
gfx_copyrect(vc->graphics->vc_gfx, 0, y_src * vc->charh, vc->graphics->vc_gfx->width,
line_count * vc->charh, 0, y_dest * vc->charh);
// Restore the cursor.
vc_set_cursor_hidden(vc, old_hide_cursor);
vc_status_update();
vc_gfx_invalidate_status(vc->graphics);
vc_invalidate_lines(vc, 0, vc_rows(vc));
}
}
static void vc_tc_setparam(void* cookie, int param, uint8_t* arg, size_t arglen) {
vc_t* vc = reinterpret_cast<vc_t*>(cookie);
switch (param) {
case TC_SET_TITLE:
strncpy(vc->title, (char*)arg, sizeof(vc->title));
vc->title[sizeof(vc->title) - 1] = '\0';
vc_status_update();
if (g_vc_owns_display && vc_graphics_enabled(vc)) {
vc_gfx_invalidate_status(vc->graphics);
}
break;
case TC_SHOW_CURSOR:
vc_set_cursor_hidden(vc, false);
break;
case TC_HIDE_CURSOR:
vc_set_cursor_hidden(vc, true);
break;
default:; // nothing
}
}
static void vc_clear_gfx(vc_t* vc) {
// Fill display with background color
if (g_vc_owns_display && vc->active && vc_graphics_enabled(vc)) {
gfx_fillrect(vc->graphics->vc_gfx, 0, 0, vc->graphics->vc_gfx->width,
vc->graphics->vc_gfx->height, palette_to_color(vc, vc->back_color));
}
}
static void vc_reset(vc_t* vc) {
// reset the cursor
vc->cursor_x = 0;
vc->cursor_y = 0;
// reset the viewport position
vc->viewport_y = 0;
tc_init(&vc->textcon, vc->columns, vc_rows(vc), vc->text_buf, vc->front_color, vc->back_color,
vc->cursor_x, vc->cursor_y);
vc->textcon.cookie = vc;
vc->textcon.invalidate = vc_tc_invalidate;
vc->textcon.movecursor = vc_tc_movecursor;
vc->textcon.push_scrollback_line = vc_tc_push_scrollback_line;
vc->textcon.copy_lines = vc_tc_copy_lines;
vc->textcon.setparam = vc_tc_setparam;
// fill textbuffer with blank characters
size_t count = vc->rows * vc->columns;
vc_char_t* ptr = vc->text_buf;
while (count--) {
*ptr++ = vc_char_make(' ', vc->front_color, vc->back_color);
}
vc_clear_gfx(vc);
if (vc_graphics_enabled(vc)) {
vc_gfx_invalidate_all(vc->graphics, vc);
}
}
void vc_status_clear() {
if (g_vc_owns_display && g_active_vc && vc_graphics_enabled(g_active_vc)) {
gfx_fillrect(g_active_vc->graphics->vc_status_bar_gfx, 0, 0,
g_active_vc->graphics->vc_status_bar_gfx->width,
g_active_vc->graphics->vc_status_bar_gfx->height,
default_palette[STATUS_COLOR_BG]);
}
}
void vc_status_commit() {
if (g_vc_owns_display && g_active_vc && vc_graphics_enabled(g_active_vc)) {
vc_gfx_invalidate_status(g_active_vc->graphics);
}
}
void vc_status_write(int x, unsigned color, const char* text) {
char c;
unsigned fg = default_palette[color];
unsigned bg = default_palette[STATUS_COLOR_BG];
vc_t* vc = g_active_vc;
if (!g_active_vc) {
return;
}
if (g_vc_owns_display && vc_graphics_enabled(vc)) {
x *= vc->graphics->vc_font->width;
while ((c = *text++) != 0) {
gfx_putchar(vc->graphics->vc_status_bar_gfx, vc->graphics->vc_font, c, x, 0, fg, bg);
x += vc->graphics->vc_font->width;
}
}
}
void vc_render(vc_t* vc) {
if (g_vc_owns_display && vc->active && vc_graphics_enabled(vc)) {
vc_status_update();
vc_gfx_invalidate_all(vc->graphics, vc);
}
}
void vc_full_repaint(vc_t* vc) {
if (g_vc_owns_display && vc_graphics_enabled(vc)) {
vc_clear_gfx(vc);
int scrollback_lines = vc_get_scrollback_lines(vc);
vc_invalidate(vc, 0, -scrollback_lines, vc->columns, scrollback_lines + vc->rows);
}
}
int vc_get_scrollback_lines(vc_t* vc) { return vc->scrollback_rows_count; }
vc_char_t* vc_get_scrollback_line_ptr(vc_t* vc, unsigned row) {
assert(row < vc->scrollback_rows_count);
row += vc->scrollback_offset;
if (row >= vc->scrollback_rows_max)
row -= vc->scrollback_rows_max;
return &vc->scrollback_buf[row * vc->columns];
}
static void vc_scroll_viewport_abs(vc_t* vc, int vpy) {
vpy = MIN(vpy, 0);
vpy = MAX(vpy, -vc_get_scrollback_lines(vc));
int diff = vpy - vc->viewport_y;
if (diff == 0)
return;
int diff_abs = ABS(diff);
vc->viewport_y = vpy;
int rows = vc_rows(vc);
if (!g_vc_owns_display || !vc->active || !vc_graphics_enabled(vc)) {
return;
}
if (diff_abs >= rows) {
// We are scrolling the viewport by a large delta. Invalidate all
// of the visible area of the console.
vc_invalidate(vc, 0, vpy, vc->columns, rows);
} else {
if (diff > 0) {
gfx_copyrect(vc->graphics->vc_gfx, 0, diff_abs * vc->charh, vc->graphics->vc_gfx->width,
(rows - diff_abs) * vc->charh, 0, 0);
vc_invalidate(vc, 0, vpy + rows - diff_abs, vc->columns, diff_abs);
} else {
gfx_copyrect(vc->graphics->vc_gfx, 0, 0, vc->graphics->vc_gfx->width,
(rows - diff_abs) * vc->charh, 0, diff_abs * vc->charh);
vc_invalidate(vc, 0, vpy, vc->columns, diff_abs);
}
}
vc_render(vc);
}
void vc_scroll_viewport(vc_t* vc, int dir) { vc_scroll_viewport_abs(vc, vc->viewport_y + dir); }
void vc_scroll_viewport_top(vc_t* vc) { vc_scroll_viewport_abs(vc, INT_MIN); }
void vc_scroll_viewport_bottom(vc_t* vc) { vc_scroll_viewport_abs(vc, 0); }
void vc_set_fullscreen(vc_t* vc, bool fullscreen) {
unsigned flags;
if (fullscreen) {
flags = vc->flags | VC_FLAG_FULLSCREEN;
} else {
flags = vc->flags & ~VC_FLAG_FULLSCREEN;
}
if (flags != vc->flags) {
vc->flags = flags;
tc_seth(&vc->textcon, vc_rows(vc));
}
vc_render(vc);
}
const gfx_font* vc_get_font() { return g_font; }
void vc_attach_gfx(vc_t* vc) {
if (vc->graphics == nullptr) {
return;
}
// If the size of the new gfx console doesn't match what we had been
// attached to, we need to allocate new memory and copy the existing
// data over.
unsigned rows = vc->graphics->vc_gfx->height / vc->charh;
unsigned columns = vc->graphics->vc_gfx->width / vc->charw;
if (rows == vc->rows && columns == vc->columns) {
return;
}
// allocate the new buffers
vc_char_t* text_buf = reinterpret_cast<vc_char_t*>(calloc(1, rows * columns * sizeof(vc_char_t)));
vc_char_t* scrollback_buf = reinterpret_cast<vc_char_t*>(
calloc(1, vc->scrollback_rows_max * columns * sizeof(vc_char_t)));
if (text_buf && scrollback_buf) {
// fill new text buffer with blank characters
size_t count = rows * columns;
vc_char_t* ptr = text_buf;
while (count--) {
*ptr++ = vc_char_make(' ', vc->front_color, vc->back_color);
}
// Copy the most recent data from the old console to the new one. There are
// (vc->cursor_y + 1) rows available, and we want (rows - (vc->rows - vc_rows(vc))
// rows. Subtract to get the first row index to copy.
unsigned old_i =
MAX(static_cast<int>((vc->cursor_y + 1) - (rows - (vc->rows - vc_rows(vc)))), 0);
unsigned old_data_start = old_i;
unsigned new_i = 0;
size_t len = (vc->columns < columns ? vc->columns : columns) * sizeof(vc_char_t);
while (new_i < rows && old_i <= vc->cursor_y) {
memcpy(text_buf + columns * (new_i++), vc->text_buf + vc->columns * (old_i++), len);
}
// copy the old scrollback buffer
for (int i = 0; i < SCROLLBACK_ROWS; i++) {
memcpy(scrollback_buf + columns * i, vc->scrollback_buf + vc->columns * i, len);
}
vc_char_t* old_text_buf = vc->text_buf;
unsigned old_columns = vc->columns;
free(vc->scrollback_buf);
vc->text_buf = text_buf;
vc->scrollback_buf = scrollback_buf;
vc->rows = rows;
vc->columns = columns;
// Push any data that fell off of text_buf. Use a temporary buffer of the
// right length to handle going to a wider console. Set it to ' 's before
// pushing, so we don't merge data from old rows.
if (old_data_start) {
vc_char_t buf[columns];
for (unsigned i = 0; i < old_data_start; i++) {
vc_char_t* ptr = buf;
while (ptr < buf + columns) {
*ptr++ = vc_char_make(' ', vc->front_color, vc->back_color);
}
ptr = old_text_buf + i * old_columns;
memcpy(buf, ptr, len);
vc_tc_scrollback_buffer_push(vc, buf);
}
}
free(old_text_buf);
} else {
// If we failed to allocate new buffers, use the old ones as best we can
free(text_buf);
free(scrollback_buf);
vc->rows = MIN(vc->rows, rows);
vc->columns = MIN(vc->columns, columns);
printf("vc: buffer resize failed, reusing old buffers (%dx%d)\n", vc->rows, vc->columns);
}
vc->viewport_y = 0;
if (vc->cursor_x >= vc->columns) {
vc->cursor_x = vc->columns - 1;
}
if (static_cast<int>(vc->cursor_y) >= vc_rows(vc)) {
vc->cursor_y = vc_rows(vc) - 1;
}
tc_init(&vc->textcon, vc->columns, vc_rows(vc), vc->text_buf, vc->front_color, vc->back_color,
vc->cursor_x, vc->cursor_y);
}
zx_status_t vc_alloc(vc_t** out, const color_scheme_t* color_scheme) {
vc_t* vc = reinterpret_cast<vc_t*>(calloc(1, sizeof(vc_t)));
if (!vc) {
return ZX_ERR_NO_MEMORY;
}
vc->fd = -1;
vc->keymap = g_keymap;
vc->font = vc_get_font();
vc->charw = vc->font->width;
vc->charh = vc->font->height;
zx_status_t status = vc_setup(vc, color_scheme);
if (status != ZX_OK) {
free(vc);
return status;
}
vc_attach_to_main_display(vc);
vc_reset(vc);
*out = vc;
return ZX_OK;
}
void vc_free(vc_t* vc) {
if (vc->active) {
g_active_vc = nullptr;
vc_set_active(0, nullptr);
}
if (g_active_vc) {
vc_full_repaint(g_active_vc);
vc_render(g_active_vc);
}
if (vc->fd >= 0) {
close(vc->fd);
}
free(vc->text_buf);
free(vc->scrollback_buf);
free(vc);
}
void vc_flush(vc_t* vc) {
if (g_vc_owns_display && vc_graphics_enabled(vc) && vc->invy1 >= 0) {
int rows = vc_rows(vc);
// Adjust for the current viewport position. Convert
// console-relative row numbers to screen-relative row numbers.
int invalidate_y0 = MIN(vc->invy0 - vc->viewport_y, rows);
int invalidate_y1 = MIN(vc->invy1 - vc->viewport_y, rows);
vc_gfx_invalidate(vc->graphics, vc, 0, invalidate_y0, vc->columns,
invalidate_y1 - invalidate_y0);
}
}
void vc_flush_all(vc_t* vc) {
if (g_vc_owns_display && vc_graphics_enabled(vc)) {
vc_gfx_invalidate_all(vc->graphics, vc);
}
}
void vc_device_init(const gfx_font* font, const keychar_t* keymap) {
g_font = font;
g_keymap = keymap;
}