forked from Lephenixnoir/PythonExtra
466 lines
13 KiB
C
466 lines
13 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/defs/util.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include "console.h"
|
|
|
|
#include "py/mphal.h"
|
|
#include "../../shared/readline/readline.h"
|
|
#include "keymap.h"
|
|
|
|
//=== Dynamic console lines ===//
|
|
|
|
bool console_line_init(console_line_t *line, int prealloc_size)
|
|
{
|
|
char *data = malloc(prealloc_size + 1);
|
|
if(!data)
|
|
return false;
|
|
|
|
data[0] = 0;
|
|
line->data = data;
|
|
line->size = 0;
|
|
line->alloc_size = prealloc_size + 1;
|
|
line->render_lines = 0;
|
|
return true;
|
|
}
|
|
|
|
void console_line_deinit(console_line_t *line)
|
|
{
|
|
free(line->data);
|
|
line->data = NULL;
|
|
line->size = 0;
|
|
line->alloc_size = 0;
|
|
}
|
|
|
|
bool console_line_alloc(console_line_t *line, int n)
|
|
{
|
|
if(line->alloc_size >= n + 1)
|
|
return true;
|
|
|
|
/* Always increase the size by at least 16 so we can insert many times in a
|
|
row without worrying about excessive insertions. */
|
|
int newsize = max(line->alloc_size + 16, n + 1);
|
|
char *newdata = realloc(line->data, newsize);
|
|
if(!newdata)
|
|
return false;
|
|
|
|
line->data = newdata;
|
|
line->alloc_size = newsize;
|
|
return true;
|
|
}
|
|
|
|
int console_line_capacity(console_line_t *line)
|
|
{
|
|
return PE_CONSOLE_LINE_MAX_LENGTH - line->size;
|
|
}
|
|
|
|
bool console_line_insert(console_line_t *line, int p, char const *str, int n)
|
|
{
|
|
if(p < 0 || p > line->size || n > console_line_capacity(line))
|
|
return false;
|
|
if(!console_line_alloc(line, line->size + n))
|
|
return false;
|
|
|
|
/* Move the end of the string (plus the NUL) n bytes forward */
|
|
memmove(line->data + p + n, line->data + p, line->size - p + 1);
|
|
memcpy(line->data + p, str, n);
|
|
line->size += n;
|
|
return true;
|
|
}
|
|
|
|
void console_line_delete(console_line_t *line, int p, int n)
|
|
{
|
|
n = min(n, line->size - p);
|
|
|
|
/* Move the end of the string (plus the NUL) n bytes backwards */
|
|
memmove(line->data + p, line->data + p + n, line->size - n - p + 1);
|
|
line->size -= n;
|
|
}
|
|
|
|
void console_line_update_render_lines(console_line_t *line, int width)
|
|
{
|
|
line->render_lines = 0;
|
|
|
|
char const *p = line->data;
|
|
char const *endline = p + strlen(p);
|
|
|
|
if(endline == p) {
|
|
line->render_lines = 1;
|
|
return;
|
|
}
|
|
|
|
while(p < endline) {
|
|
line->render_lines++;
|
|
p = drsize(p, NULL, width, NULL);
|
|
}
|
|
}
|
|
|
|
int console_line_render(int x, int y, console_line_t *line, int w, int dy,
|
|
int show_from, int show_until, int cursor)
|
|
{
|
|
char const *p = line->data;
|
|
char const *endline = p + strlen(p);
|
|
int line_offset = 0;
|
|
int line_number = 0;
|
|
|
|
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;
|
|
}
|
|
|
|
//=== Terminal emulator ===//
|
|
|
|
console_t *console_create(int backlog_size)
|
|
{
|
|
console_t *cons = malloc(sizeof *cons);
|
|
cons->lines = NULL;
|
|
cons->line_count = 0;
|
|
|
|
cons->absolute_lineno = 1;
|
|
cons->backlog_size = max(backlog_size, PE_CONSOLE_LINE_MAX_LENGTH);
|
|
|
|
cons->render_needed = true;
|
|
cons->render_font = NULL;
|
|
cons->render_width = 0;
|
|
cons->render_lines = 0;
|
|
cons->render_total_lines = 0;
|
|
|
|
if(!console_new_line(cons)) {
|
|
free(cons);
|
|
return NULL;
|
|
}
|
|
return cons;
|
|
}
|
|
|
|
bool console_new_line(console_t *cons)
|
|
{
|
|
int newsize = (cons->line_count + 1) * sizeof *cons->lines;
|
|
console_line_t *newlines = realloc(cons->lines, newsize);
|
|
if(!newlines)
|
|
return false;
|
|
cons->lines = newlines;
|
|
|
|
/* If this fails we keep the extended lines buffer. The next realloc() will
|
|
be a no-op and we don't have to track anything. */
|
|
if(!console_line_init(&cons->lines[cons->line_count], 16))
|
|
return false;
|
|
cons->line_count++;
|
|
cons->cursor = 0;
|
|
cons->render_needed = true;
|
|
|
|
/* Routinely clean the backlog every time a line is added. */
|
|
// console_clean_backlog(cons);
|
|
return true;
|
|
}
|
|
|
|
/* static console_line_t *console_get_absolute_line(console_t const *cons,
|
|
int lineno)
|
|
{
|
|
int i = lineno - cons->absolute_lineno;
|
|
return (i >= 0 && i < cons->line_count) ? &cons->lines[i] : NULL;
|
|
} */
|
|
|
|
void console_clean_backlog(console_t *cons)
|
|
{
|
|
/* Find how many lines we can keep while using at most cons->backlog_size
|
|
bytes to store them. The console must always have at least one line. */
|
|
int first_kept = cons->line_count;
|
|
int line_size = 0;
|
|
int total_size = 0;
|
|
|
|
do {
|
|
total_size += line_size;
|
|
first_kept--;
|
|
line_size = cons->lines[first_kept].size;
|
|
}
|
|
while(first_kept > 0 && total_size + line_size <= cons->backlog_size);
|
|
|
|
/* Remove old lines */
|
|
for(int j = 0; j < first_kept; j++)
|
|
console_line_deinit(&cons->lines[j]);
|
|
|
|
/* Don't realloc() yet, it will happen soon enough anyway */
|
|
memmove(cons->lines, cons->lines + first_kept,
|
|
(cons->line_count - first_kept) * sizeof cons->lines[0]);
|
|
cons->line_count -= first_kept;
|
|
|
|
/* Update the absolute number of the first line */
|
|
cons->absolute_lineno += first_kept;
|
|
}
|
|
|
|
void console_clear_render_flag(console_t *cons)
|
|
{
|
|
cons->render_needed = false;
|
|
}
|
|
|
|
void console_destroy(console_t *cons)
|
|
{
|
|
for(int i = 0; i < cons->line_count; i++)
|
|
console_line_deinit(&cons->lines[i]);
|
|
free(cons->lines);
|
|
free(cons);
|
|
}
|
|
|
|
//=== Rendering functions ===//
|
|
|
|
/* Compute the shell's horizontal layout as:
|
|
1. Text width (region where the text is renderer)
|
|
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
|
|
}
|
|
|
|
void console_compute_view(console_t *cons, font_t const *font,
|
|
int width, int lines)
|
|
{
|
|
cons->render_font = font;
|
|
cons->render_width = width;
|
|
cons->render_lines = lines;
|
|
cons->render_total_lines = 0;
|
|
|
|
int text_w;
|
|
view_params(width, &text_w, NULL, NULL);
|
|
|
|
for(int i = 0; i < cons->line_count; i++) {
|
|
console_line_update_render_lines(&cons->lines[i], text_w);
|
|
cons->render_total_lines += cons->lines[i].render_lines;
|
|
}
|
|
}
|
|
|
|
console_scrollpos_t console_clamp_scrollpos(console_t const *cons,
|
|
console_scrollpos_t pos)
|
|
{
|
|
/* No scrolling case */
|
|
if(cons->render_total_lines < cons->render_lines)
|
|
return 0;
|
|
|
|
/* Normal clamp */
|
|
return max(0, min(pos, cons->render_total_lines - cons->render_lines));
|
|
}
|
|
|
|
void console_render(int x, int y0, console_t const *cons, int dy,
|
|
console_scrollpos_t pos)
|
|
{
|
|
int total_lines = cons->render_total_lines;
|
|
int visible_lines = cons->render_lines;
|
|
int w = cons->render_width;
|
|
int watermark = visible_lines - total_lines + pos;
|
|
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);
|
|
|
|
/* Show only visible lines */
|
|
for(int i = 0; i < cons->line_count; i++) {
|
|
console_line_t *line = &cons->lines[i];
|
|
bool show_cursor = (i == cons->line_count - 1);
|
|
|
|
if(watermark + line->render_lines > 0)
|
|
y = console_line_render(x, y, line, text_w, dy, -watermark,
|
|
visible_lines - watermark, show_cursor ? cons->cursor : -1);
|
|
watermark += line->render_lines;
|
|
}
|
|
|
|
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 ===//
|
|
|
|
bool console_write_block_at_cursor(console_t *cons, char const *str, int n)
|
|
{
|
|
if(!cons->line_count && !console_new_line(cons))
|
|
return false;
|
|
|
|
/* Split up n characters into chunks that remain within each line's storage
|
|
capacity. */
|
|
while(n > 0) {
|
|
console_line_t *last_line = &cons->lines[cons->line_count - 1];
|
|
int capacity = console_line_capacity(last_line);
|
|
int round_size = min(n, capacity);
|
|
|
|
if(!console_line_insert(last_line, cons->cursor, str, round_size))
|
|
return false;
|
|
cons->cursor += round_size;
|
|
cons->render_needed = true;
|
|
if(round_size < n && !console_new_line(cons))
|
|
return false;
|
|
n -= round_size;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool console_write_at_cursor(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_block_at_cursor(cons, buf + offset, line_size))
|
|
return false;
|
|
|
|
offset += line_size;
|
|
if(offset >= n)
|
|
break;
|
|
|
|
if(buf[offset] == '\n') {
|
|
if(!console_new_line(cons))
|
|
return false;
|
|
offset++;
|
|
}
|
|
else if(buf[offset] == 8) {
|
|
offset++;
|
|
console_line_t *last_line = &cons->lines[cons->line_count - 1];
|
|
if(cons->cursor > 0) {
|
|
console_line_delete(last_line, cons->cursor-1, 1);
|
|
cons->cursor--;
|
|
}
|
|
}
|
|
else if(buf[offset] == '\e') {
|
|
console_line_t *last_line = &cons->lines[cons->line_count - 1];
|
|
offset++;
|
|
|
|
/* TODO: Handle more complex escape sequences */
|
|
if(offset + 2 <= n && buf[offset] == '[' && buf[offset+1] == 'K') {
|
|
console_line_delete(last_line, cons->cursor,
|
|
last_line->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 < last_line->size);
|
|
}
|
|
}
|
|
|
|
cons->render_needed = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void console_clear_current_line(console_t *cons)
|
|
{
|
|
console_line_t *last_line = &cons->lines[cons->line_count - 1];
|
|
console_line_delete(last_line, 0, last_line->size);
|
|
cons->cursor = 0;
|
|
}
|
|
|
|
//=== Input method ===//
|
|
|
|
/* Features needed to bypass MicroPython's readline:
|
|
* Undefined MICROPY_HAL_HAS_VT100
|
|
* Multi-line input
|
|
- Provide PS1 and PS2
|
|
- Auto-indent
|
|
- TODO: How is it stored?
|
|
* History
|
|
- Let's use `MP_STATE_PORT(readline_hist)` for a start
|
|
- Keep track of history browsing state
|
|
- While at it do the zsh history search, which is goated
|
|
- Use readline_push_history()
|
|
* Cursor movement (fairly easy)
|
|
* Handle special inputs
|
|
- ^C, ^D, backspace
|
|
- Erase line (^K)
|
|
* Autocompletion
|
|
- Use mp_repl_autocomplete() which should hook just fine */
|
|
|
|
int console_key_event_to_char(key_event_t ev)
|
|
{
|
|
int key = ev.key;
|
|
|
|
/* (The following meanings are only for non-empty lines) */
|
|
/* TODO: Check cons->cursor before triggering them */
|
|
|
|
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);
|
|
}
|