553 lines
15 KiB
C
553 lines
15 KiB
C
//---------------------------------------------------------------------------//
|
|
// ____ PythonExtra //
|
|
//.-'`_ o `;__, A community port of MicroPython for CASIO calculators. //
|
|
//.-'` `---` ' License: MIT (except some files; see LICENSE) //
|
|
//---------------------------------------------------------------------------//
|
|
|
|
#include <gint/keyboard.h>
|
|
#include <gint/display.h>
|
|
#include <gint/kmalloc.h>
|
|
#include <gint/defs/util.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "console.h"
|
|
|
|
#include "py/mphal.h"
|
|
#include "../../shared/readline/readline.h"
|
|
#include "keymap.h"
|
|
|
|
/* Compute the shell's horizontal layout as:
|
|
1. Text width (region where the text is rendered)
|
|
2. Spacing left of the scrollbar
|
|
3. Width of the scrollbar */
|
|
static void view_params(int w,
|
|
int *text_w, int *scroll_spacing, int *scroll_width)
|
|
{
|
|
#ifdef FX9860G
|
|
if(text_w)
|
|
*text_w = w - 2;
|
|
if(scroll_spacing)
|
|
*scroll_spacing = 1;
|
|
if(scroll_width)
|
|
*scroll_width = 1;
|
|
#else
|
|
if(text_w)
|
|
*text_w = w - 4;
|
|
if(scroll_spacing)
|
|
*scroll_spacing = 2;
|
|
if(scroll_width)
|
|
*scroll_width = 2;
|
|
#endif
|
|
}
|
|
|
|
//=== Static console lines ===//
|
|
|
|
void console_fline_update_render_lines(console_fline_t *FL, int width)
|
|
{
|
|
char const *p = FL->data;
|
|
FL->render_lines = 0;
|
|
|
|
do {
|
|
FL->render_lines++;
|
|
p = drsize(p, NULL, width, NULL);
|
|
}
|
|
while(*p);
|
|
}
|
|
|
|
int console_fline_render(int x, int y, console_fline_t *FL, int w, int dy,
|
|
int show_from, int show_until, int cursor)
|
|
{
|
|
char const *p = FL->data;
|
|
char const *endline = p + FL->size;
|
|
int line_offset = 0;
|
|
int line_number = 0;
|
|
|
|
if(p == endline && cursor == 0) {
|
|
int h;
|
|
dsize("", NULL, NULL, &h);
|
|
dline(x, y, x, y+h-1, C_BLACK);
|
|
}
|
|
while(p < endline) {
|
|
char const *endscreen = drsize(p, NULL, w, NULL);
|
|
int len = endscreen - p;
|
|
|
|
if(line_number >= show_from && line_number < show_until) {
|
|
dtext_opt(x, y, C_BLACK, C_NONE, DTEXT_LEFT, DTEXT_TOP, p, len);
|
|
if(cursor >= line_offset && cursor <= line_offset + len) {
|
|
int w, h;
|
|
dnsize(p, cursor - line_offset, NULL, &w, &h);
|
|
dline(x+w, y, x+w, y+h-1, C_BLACK);
|
|
}
|
|
y += dy;
|
|
}
|
|
|
|
p += len;
|
|
line_offset += len;
|
|
line_number++;
|
|
}
|
|
|
|
return y;
|
|
}
|
|
|
|
//=== Rotating line storage ===//
|
|
|
|
bool linebuf_init(linebuf_t *buf, int capacity, int backlog_size)
|
|
{
|
|
if(capacity <= 0)
|
|
return false;
|
|
|
|
buf->lines = kmalloc(capacity * sizeof *buf->lines, PE_CONSOLE_LINE_ALLOC);
|
|
if(!buf->lines)
|
|
return false;
|
|
memset(buf->lines, 0, capacity * sizeof *buf->lines);
|
|
|
|
stredit_init(&buf->edit, 0, 0, 0);
|
|
|
|
buf->capacity = capacity;
|
|
buf->start = 0;
|
|
buf->size = 0;
|
|
buf->absolute = 1;
|
|
buf->backlog_size = backlog_size;
|
|
buf->total_size_except_last = 0;
|
|
buf->absolute_rendered = 0;
|
|
buf->total_rendered = 0;
|
|
return true;
|
|
}
|
|
|
|
void linebuf_deinit(linebuf_t *buf)
|
|
{
|
|
stredit_reset(&buf->edit);
|
|
for(int i = 0; i < buf->capacity; i++)
|
|
free(buf->lines[i]);
|
|
kfree((void *)buf->lines);
|
|
memset(buf, 0, sizeof *buf);
|
|
}
|
|
|
|
/* Lines in the buffer are identified by their positions within the `lines`
|
|
array, which are integers equipped with modulo arithmetic (we call them
|
|
"indices"). We abstract away the rotation by numbering stored lines from 0
|
|
to buf->size - 1, and we call these numbers "nths". When we want to identify
|
|
lines independent of rotation, we use their absolute line number.
|
|
|
|
Such numbers refer to lines stored in the buffer if:
|
|
(index) 0 <= index < size
|
|
(nth) 0 <= nth < size
|
|
(abs) 0 <= abs - buf->absolute < size */
|
|
|
|
GINLINE static int linebuf_nth_to_index(linebuf_t const *buf, int nth)
|
|
{
|
|
return (buf->start + nth) % buf->capacity;
|
|
}
|
|
|
|
/* Get the nth line. */
|
|
GINLINE static console_fline_t *linebuf_get_nth_line(linebuf_t const *buf,
|
|
int nth)
|
|
{
|
|
if(nth < 0 || nth >= buf->size)
|
|
return NULL;
|
|
|
|
if(nth == buf->size - 1)
|
|
return (console_fline_t *)buf->edit.raw;
|
|
else
|
|
return buf->lines[linebuf_nth_to_index(buf, nth)];
|
|
}
|
|
|
|
/* Move `index` by `diff`; assumes |diff| <= buf->capacity. */
|
|
GINLINE static int linebuf_index_add(linebuf_t const *buf, int index, int diff)
|
|
{
|
|
return (index + diff + buf->capacity) % buf->capacity;
|
|
}
|
|
|
|
int linebuf_start(linebuf_t const *buf)
|
|
{
|
|
return buf->absolute;
|
|
}
|
|
|
|
int linebuf_end(linebuf_t const *buf)
|
|
{
|
|
return buf->absolute + buf->size;
|
|
}
|
|
|
|
console_fline_t *linebuf_get_line(linebuf_t const *buf, int abs)
|
|
{
|
|
return linebuf_get_nth_line(buf, abs - buf->absolute);
|
|
}
|
|
|
|
stredit_t *linebuf_get_last_line(linebuf_t *buf)
|
|
{
|
|
return (buf->size > 0) ? &buf->edit : NULL;
|
|
}
|
|
|
|
stredit_t *linebuf_newline(linebuf_t *buf)
|
|
{
|
|
/* Make space if the buffer is full */
|
|
linebuf_recycle_oldest_lines(buf, buf->size - buf->capacity + 1);
|
|
|
|
/* Freeze the current last line and reset the editor */
|
|
if(buf->size > 0) {
|
|
buf->total_size_except_last += buf->edit.size;
|
|
|
|
int size = buf->edit.size;
|
|
console_fline_t *frozen = (void *)stredit_freeze_and_reset(&buf->edit);
|
|
frozen->size = size;
|
|
|
|
int last_nth = linebuf_nth_to_index(buf, buf->size - 1);
|
|
assert(buf->lines[last_nth] == NULL);
|
|
buf->lines[last_nth] = frozen;
|
|
}
|
|
|
|
buf->size++;
|
|
int last_nth = linebuf_nth_to_index(buf, buf->size - 1);
|
|
buf->lines[last_nth] = NULL;
|
|
stredit_init(&buf->edit, 16, CONSOLE_FLINE_SIZE,
|
|
PE_CONSOLE_LINE_MAX_LENGTH);
|
|
return &buf->edit;
|
|
}
|
|
|
|
void linebuf_recycle_oldest_lines(linebuf_t *buf, int count)
|
|
{
|
|
count = min(count, buf->size);
|
|
if(count <= 0)
|
|
return;
|
|
|
|
for(int nth = 0; nth < count; nth++) {
|
|
console_fline_t *FL = linebuf_get_nth_line(buf, nth);
|
|
buf->total_rendered -= FL->render_lines;
|
|
if(nth != buf->size - 1)
|
|
buf->total_size_except_last -= FL->size;
|
|
free(FL);
|
|
}
|
|
|
|
buf->start = linebuf_index_add(buf, buf->start, count);
|
|
buf->size -= count;
|
|
buf->absolute += count;
|
|
}
|
|
|
|
void linebuf_clean_backlog(linebuf_t *buf)
|
|
{
|
|
if(buf->size <= 0)
|
|
return;
|
|
|
|
int remove = 0;
|
|
int n = buf->total_size_except_last + linebuf_get_last_line(buf)->size;
|
|
|
|
while(remove < buf->size - 1 && n > buf->backlog_size) {
|
|
n -= linebuf_get_nth_line(buf, remove)->size;
|
|
remove++;
|
|
}
|
|
|
|
linebuf_recycle_oldest_lines(buf, remove);
|
|
}
|
|
|
|
void linebuf_update_render(linebuf_t *buf, int width, bool lazy)
|
|
{
|
|
int start = linebuf_start(buf);
|
|
int end = linebuf_end(buf);
|
|
if(lazy)
|
|
start = max(start, buf->absolute_rendered + 1);
|
|
|
|
int text_w;
|
|
view_params(width, &text_w, NULL, NULL);
|
|
|
|
for(int abs = start; abs < end; abs++) {
|
|
console_fline_t *FL = linebuf_get_nth_line(buf, abs - buf->absolute);
|
|
buf->total_rendered -= FL->render_lines;
|
|
console_fline_update_render_lines(FL, text_w);
|
|
buf->total_rendered += FL->render_lines;
|
|
}
|
|
|
|
buf->absolute_rendered = max(buf->absolute_rendered, end - 2);
|
|
}
|
|
|
|
//=== Terminal emulator ===//
|
|
|
|
console_t *console_create(int backlog_size, int maximum_line_count)
|
|
{
|
|
backlog_size = max(backlog_size, PE_CONSOLE_LINE_MAX_LENGTH);
|
|
maximum_line_count = max(maximum_line_count, 1);
|
|
|
|
console_t *cons = malloc(sizeof *cons);
|
|
if(!linebuf_init(&cons->lines, maximum_line_count, backlog_size)) {
|
|
free(cons);
|
|
return NULL;
|
|
}
|
|
|
|
cons->cursor = -1;
|
|
cons->render_needed = true;
|
|
cons->render_font = NULL;
|
|
cons->render_width = 0;
|
|
cons->render_lines = 0;
|
|
|
|
console_newline(cons);
|
|
return cons;
|
|
}
|
|
|
|
void console_newline(console_t *cons)
|
|
{
|
|
linebuf_newline(&cons->lines);
|
|
cons->cursor = 0;
|
|
cons->render_needed = true;
|
|
|
|
/* This is a good time to clean up the backlog. */
|
|
// TODO: This might actually be the performance bottleneck, because we do a
|
|
// long loop only to find out that the total size is still reasonable!
|
|
linebuf_clean_backlog(&cons->lines);
|
|
}
|
|
|
|
void console_clear_render_flag(console_t *cons)
|
|
{
|
|
cons->render_needed = false;
|
|
}
|
|
|
|
void console_destroy(console_t *cons)
|
|
{
|
|
linebuf_deinit(&cons->lines);
|
|
free(cons);
|
|
}
|
|
|
|
//=== Rendering functions ===//
|
|
|
|
void console_compute_view(console_t *cons, font_t const *font,
|
|
int width, int lines)
|
|
{
|
|
/* If a view with the same width was previously computed, do a lazy
|
|
update: recompute only the last lines. */
|
|
bool lazy = (width != 0 && cons->render_width == width);
|
|
cons->render_font = font;
|
|
cons->render_width = width;
|
|
cons->render_lines = lines;
|
|
|
|
font_t const *old_font = dfont(font);
|
|
linebuf_update_render(&cons->lines, width, lazy);
|
|
dfont(old_font);
|
|
}
|
|
|
|
console_scrollpos_t console_clamp_scrollpos(console_t const *cons,
|
|
console_scrollpos_t pos)
|
|
{
|
|
/* No scrolling case */
|
|
if(cons->lines.total_rendered < cons->render_lines)
|
|
return 0;
|
|
|
|
return max(0, min(pos, cons->lines.total_rendered - cons->render_lines));
|
|
}
|
|
|
|
void console_render(int x, int y0, console_t *cons, int dy,
|
|
console_scrollpos_t pos)
|
|
{
|
|
int total_lines = cons->lines.total_rendered;
|
|
int visible_lines = cons->render_lines;
|
|
int w = cons->render_width;
|
|
int y = y0;
|
|
|
|
int text_w, scroll_spacing, scroll_w;
|
|
view_params(w, &text_w, &scroll_spacing, &scroll_w);
|
|
|
|
font_t const *old_font = dfont(cons->render_font);
|
|
|
|
/* Normally only frozen lines have a valid size field. Update the size of
|
|
the last line so we don't have to make an exception for it. */
|
|
stredit_t *ed = linebuf_get_last_line(&cons->lines);
|
|
console_fline_t *ed_FL = (console_fline_t *)ed->raw;
|
|
ed_FL->size = ed->size;
|
|
|
|
/* Show only visible lines. We want to avoid counting all the lines in the
|
|
console, and instead start from the end. */
|
|
int line_y = visible_lines + pos;
|
|
int L_start = linebuf_start(&cons->lines);
|
|
int L_end = linebuf_end(&cons->lines);
|
|
int i = linebuf_end(&cons->lines);
|
|
|
|
while(i > L_start && line_y > 0)
|
|
line_y -= linebuf_get_line(&cons->lines, --i)->render_lines;
|
|
|
|
/* If there isn't enough content to fill the view, start at the top. */
|
|
line_y = min(line_y, pos);
|
|
|
|
while(i < L_end && line_y < visible_lines) {
|
|
console_fline_t *FL = linebuf_get_line(&cons->lines, i);
|
|
bool show_cursor = (i == L_end - 1);
|
|
|
|
y = console_fline_render(x, y, FL, text_w, dy, -line_y,
|
|
visible_lines - line_y, show_cursor ? cons->cursor : -1);
|
|
line_y += FL->render_lines;
|
|
i++;
|
|
}
|
|
|
|
dfont(old_font);
|
|
|
|
/* Scrollbar */
|
|
if(total_lines > visible_lines) {
|
|
int first_shown = total_lines - visible_lines - pos;
|
|
int h = dy * visible_lines;
|
|
int y1 = y0 + h * first_shown / total_lines;
|
|
int y2 = y0 + h * (first_shown + visible_lines) / total_lines;
|
|
|
|
int color = C_BLACK;
|
|
#ifdef FXCG50
|
|
if(pos == 0) color = C_RGB(24, 24, 24);
|
|
#endif
|
|
drect(x + text_w + scroll_spacing, y1,
|
|
x + text_w + scroll_spacing + scroll_w - 1, y2,
|
|
color);
|
|
}
|
|
}
|
|
|
|
//=== Edition functions ===//
|
|
|
|
GINLINE static stredit_t *last_line(console_t *cons)
|
|
{
|
|
return linebuf_get_last_line(&cons->lines);
|
|
}
|
|
|
|
bool console_set_cursor(console_t *cons, int pos)
|
|
{
|
|
stredit_t *ed = last_line(cons);
|
|
|
|
if(pos < ed->prefix || pos > ed->size)
|
|
return false;
|
|
|
|
cons->cursor = pos;
|
|
return true;
|
|
}
|
|
|
|
bool console_move_cursor(console_t *cons, int cursor_movement)
|
|
{
|
|
return console_set_cursor(cons, cons->cursor + cursor_movement);
|
|
}
|
|
|
|
char *console_get_line(console_t *cons, bool copy)
|
|
{
|
|
stredit_t *ed = last_line(cons);
|
|
char *str = stredit_data(ed) + ed->prefix;
|
|
return copy ? strdup(str) : str;
|
|
}
|
|
|
|
bool console_write_raw(console_t *cons, char const *str, int n)
|
|
{
|
|
if(!cons->lines.size)
|
|
console_newline(cons);
|
|
|
|
/* Split string into chunks smaller than each line's storage capacity. */
|
|
while(n > 0) {
|
|
stredit_t *ed = last_line(cons);
|
|
int capacity = stredit_capacity(ed);
|
|
int round_size = min(n, capacity);
|
|
|
|
if(!stredit_insert(ed, cons->cursor, str, round_size))
|
|
return false;
|
|
cons->cursor += round_size;
|
|
cons->render_needed = true;
|
|
if(round_size < n)
|
|
console_newline(cons);
|
|
n -= round_size;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool console_write(console_t *cons, char const *buf, int n)
|
|
{
|
|
int offset = 0;
|
|
if(n < 0)
|
|
n = strlen(buf);
|
|
|
|
while(offset < n) {
|
|
/* Find the first '\n', '\e' or end of buffer */
|
|
char const *end = buf + offset;
|
|
while(end < buf + n && *end != '\n' && *end != '\e' && *end != 8)
|
|
end++;
|
|
|
|
int line_size = end - (buf + offset);
|
|
if(!console_write_raw(cons, buf + offset, line_size))
|
|
return false;
|
|
|
|
offset += line_size;
|
|
if(offset >= n)
|
|
break;
|
|
|
|
if(buf[offset] == '\n') {
|
|
console_newline(cons);
|
|
offset++;
|
|
}
|
|
else if(buf[offset] == 8) {
|
|
offset++;
|
|
stredit_t *ed = last_line(cons);
|
|
if(cons->cursor > 0) {
|
|
stredit_delete(ed, cons->cursor-1, 1);
|
|
cons->cursor--;
|
|
}
|
|
}
|
|
else if(buf[offset] == '\e') {
|
|
stredit_t *ed = last_line(cons);
|
|
offset++;
|
|
|
|
/* TODO: Handle more complex escape sequences */
|
|
if(offset + 2 <= n && buf[offset] == '[' && buf[offset+1] == 'K') {
|
|
stredit_delete(ed, cons->cursor, ed->size - cons->cursor);
|
|
offset += 2;
|
|
}
|
|
if(offset + 2 <= n && buf[offset] == 1 && buf[offset+1] == 'D') {
|
|
cons->cursor -= (cons->cursor > 0);
|
|
}
|
|
if(offset + 2 <= n && buf[offset] == 1 && buf[offset+1] == 'C') {
|
|
cons->cursor += (cons->cursor < ed->size);
|
|
}
|
|
}
|
|
|
|
cons->render_needed = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void console_lock_prefix(console_t *cons)
|
|
{
|
|
stredit_set_prefix(last_line(cons), cons->cursor);
|
|
}
|
|
|
|
void console_delete_at_cursor(console_t *cons, int n)
|
|
{
|
|
int real_n = stredit_delete(last_line(cons), cons->cursor - n, n);
|
|
cons->cursor -= real_n;
|
|
cons->render_needed = true;
|
|
}
|
|
|
|
void console_clear_current_line(console_t *cons)
|
|
{
|
|
stredit_t *ed = last_line(cons);
|
|
stredit_delete(ed, ed->prefix, ed->size - ed->prefix);
|
|
cons->cursor = ed->prefix;
|
|
cons->render_needed = true;
|
|
}
|
|
|
|
//=== Terminal input ===//
|
|
|
|
int console_key_event_to_char(key_event_t ev)
|
|
{
|
|
int key = ev.key;
|
|
|
|
if(key == KEY_LEFT && ev.shift)
|
|
return CHAR_CTRL_A; /* go-to-start-of-line */
|
|
if(key == KEY_LEFT)
|
|
return CHAR_CTRL_B; /* go-back-one-char */
|
|
if(key == KEY_ACON)
|
|
return CHAR_CTRL_C; /* cancel */
|
|
if(key == KEY_DEL)
|
|
return 8; /* delete-at-cursor */
|
|
if(key == KEY_RIGHT && ev.shift)
|
|
return CHAR_CTRL_E; /* go-to-end-of-line */
|
|
if(key == KEY_RIGHT)
|
|
return CHAR_CTRL_F; /* go-forward-one-char */
|
|
if(key == KEY_DOWN)
|
|
return CHAR_CTRL_N; /* go to next line in history */
|
|
if(key == KEY_UP)
|
|
return CHAR_CTRL_P; /* go to previous line in history */
|
|
if(key == KEY_EXIT)
|
|
return CHAR_CTRL_D; /* eof */
|
|
if(key == KEY_EXE)
|
|
return '\r';
|
|
|
|
return keymap_translate(key, ev.shift, ev.alpha);
|
|
}
|