You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
notcurses/src/lib/reader.c

416 lines
12 KiB
C

#include "internal.h"
static void
ncreader_destroy_internal(ncreader* n){
if(n){
if(n->manage_cursor){
notcurses_cursor_disable(ncplane_notcurses(n->ncp));
}
if(ncplane_set_widget(n->ncp, NULL, NULL) == 0){
ncplane_destroy(n->ncp);
}
ncplane_destroy(n->textarea);
free(n);
}
}
void ncreader_destroy(ncreader* n, char** contents){
if(n){
if(contents){
*contents = ncreader_contents(n);
}
ncreader_destroy_internal(n);
}
}
ncreader* ncreader_create(ncplane* n, const ncreader_options* opts){
ncreader_options zeroed = {0};
if(!opts){
opts = &zeroed;
}
if(opts->flags > NCREADER_OPTION_CURSOR){
logwarn("provided unsupported flags %016" PRIx64, opts->flags);
}
ncreader* nr = malloc(sizeof(*nr));
if(nr == NULL){
ncplane_destroy(n);
return NULL;
}
nr->ncp = n;
// do *not* bind it to the visible plane; we always want it offscreen,
// to the upper left of the true origin
struct ncplane_options nopts = {
.y = -ncplane_dim_y(n),
.x = -ncplane_dim_x(n),
.rows = ncplane_dim_y(n),
.cols = ncplane_dim_x(n),
.name = "text",
};
if((nr->textarea = ncplane_create(notcurses_stdplane(ncplane_notcurses(n)), &nopts)) == NULL){
ncplane_destroy(nr->ncp);
free(nr);
return NULL;
}
nr->horscroll = opts->flags & NCREADER_OPTION_HORSCROLL;
nr->xproject = 0;
nr->tchannels = opts->tchannels;
nr->tattrs = opts->tattrword;
nr->no_cmd_keys = opts->flags & NCREADER_OPTION_NOCMDKEYS;
nr->manage_cursor = opts->flags & NCREADER_OPTION_CURSOR;
ncplane_set_channels(nr->ncp, opts->tchannels);
ncplane_set_styles(nr->ncp, opts->tattrword);
if(ncplane_set_widget(n, nr, (void(*)(void*))ncreader_destroy_internal)){
ncplane_destroy(nr->textarea);
ncplane_destroy(nr->ncp);
free(nr);
return NULL;
}
return nr;
}
// empty both planes of all input, and home the cursors.
int ncreader_clear(ncreader* n){
ncplane_erase(n->ncp);
ncplane_erase(n->textarea);
n->xproject = 0;
return 0;
}
ncplane* ncreader_plane(ncreader* n){
return n->ncp;
}
// copy the viewed area down from the textarea
static int
ncreader_redraw(ncreader* n){
int ret = 0;
//fprintf(stderr, "redraw: xproj %d\n", n->xproject);
//notcurses_debug(n->ncp->nc, stderr);
assert(n->xproject >= 0);
assert(n->textarea->lenx >= n->ncp->lenx);
assert(n->textarea->leny >= n->ncp->leny);
for(unsigned y = 0 ; y < n->ncp->leny ; ++y){
const unsigned texty = y;
for(unsigned x = 0 ; x < n->ncp->lenx ; ++x){
const unsigned textx = x + n->xproject;
const nccell* src = &n->textarea->fb[nfbcellidx(n->textarea, texty, textx)];
nccell* dst = &n->ncp->fb[nfbcellidx(n->ncp, y, x)];
//fprintf(stderr, "projecting %d/%d [%s] to %d/%d [%s]\n", texty, textx, cell_extended_gcluster(n->textarea, src), y, x, cell_extended_gcluster(n->ncp, dst));
if(cellcmp_and_dupfar(&n->ncp->pool, dst, n->textarea, src) < 0){
ret = -1;
}
}
}
if(notcurses_cursor_enable(ncplane_notcurses(n->ncp), n->ncp->absy + n->ncp->y, n->ncp->absx + n->ncp->x)){
ret = -1;
}
return ret;
}
// try to move left. does not move past the start of the textarea, but will
// try to move up and to the end of the previous row if not on the top row.
// if on the left side of the viewarea, but not the left side of the textarea,
// scrolls left. returns 0 if a move was made.
int ncreader_move_left(ncreader* n){
int viewx = n->ncp->x;
int textx = n->textarea->x;
int y = n->ncp->y;
//fprintf(stderr, "moving left: tcurs: %dx%d vcurs: %dx%d xproj: %d\n", y, textx, y, viewx, n->xproject);
if(textx == 0){
// are we on the first column of the textarea? if so, we must also be on
// the first column of the viewarea. try to move up.
if(y == 0){
return -1; // no move possible
}
viewx = n->ncp->lenx - 1; // FIXME find end of particular row
--y;
textx = n->textarea->lenx - 1;
n->xproject = n->textarea->x - n->ncp->x;
}else{
// if we're on the first column of the viewarea, but not the first column
// of the textarea, we must be able to scroll to the left. do so.
// if we're not on the last column anywhere, move cursor right everywhere.
if(viewx == 0){
--n->xproject;
}else{
--viewx;
}
--textx;
}
ncplane_cursor_move_yx(n->textarea, y, textx);
ncplane_cursor_move_yx(n->ncp, y, viewx);
//fprintf(stderr, "moved left: tcurs: %dx%d vcurs: %dx%d xproj: %d\n", y, textx, y, viewx, n->xproject);
ncreader_redraw(n);
return 0;
}
// try to move right. does not move past the end of the textarea, but will
// try to move down and to the start of the previous row if not on the bottom
// row. if on the right side of the viewarea, but not the right side of the
// textarea, pans right. returns 0 if a move was made.
int ncreader_move_right(ncreader* n){
unsigned textx = n->textarea->x;
unsigned y = n->ncp->y;
unsigned viewx = n->ncp->x;
//fprintf(stderr, "moving right: tcurs: %dx%d vcurs: %dx%d xproj: %d\n", y, textx, y, viewx, n->xproject);
if(textx >= n->textarea->lenx - 1){
// are we on the last column of the textarea? if so, we must also be on
// the first column of the viewarea. try to move down.
if(y >= n->textarea->leny - 1){
return -1; // no move possible
}
viewx = 0;
++y;
textx = viewx;
n->xproject = 0;
}else{
// if we're on the first column of the viewarea, but not the first column
// of the textarea, we must be able to scroll to the left. do so.
// if we're not on the last column anywhere, move cursor right everywhere.
if(viewx >= n->ncp->lenx - 1){
++n->xproject;
}else{
++viewx;
}
++textx;
}
ncplane_cursor_move_yx(n->textarea, y, textx);
ncplane_cursor_move_yx(n->ncp, y, viewx);
//fprintf(stderr, "moved right: tcurs: %dx%d vcurs: %dx%d xproj: %d\n", y, textx, y, viewx, n->xproject);
ncreader_redraw(n);
return 0;
}
// try to move up. does not move past the top of the textarea.
// returns 0 if a move was made.
int ncreader_move_up(ncreader* n){
int y = n->ncp->y;
if(y == 0){
// are we on the last row of the textarea? if so, we can't move.
return -1;
}
--y;
ncplane_cursor_move_yx(n->textarea, y, -1);
ncplane_cursor_move_yx(n->ncp, y, -1);
ncreader_redraw(n);
return 0;
}
// try to move down. does not move past the bottom of the textarea.
// returns 0 if a move was made.
int ncreader_move_down(ncreader* n){
unsigned y = n->ncp->y;
if(y >= n->textarea->leny - 1){
// are we on the last row of the textarea? if so, we can't move.
return -1;
}
++y;
ncplane_cursor_move_yx(n->textarea, y, -1);
ncplane_cursor_move_yx(n->ncp, y, -1);
ncreader_redraw(n);
return 0;
}
// only writing can enlarge the textarea. movement can pan, but not enlarge.
int ncreader_write_egc(ncreader* n, const char* egc){
const int cols = ncstrwidth(egc, NULL, NULL);
if(cols < 0){
logerror("fed illegal UTF-8 [%s]", egc);
return -1;
}
if(n->textarea->x >= n->textarea->lenx - cols){
if(n->horscroll){
if(ncplane_resize_simple(n->textarea, n->textarea->leny, n->textarea->lenx + cols)){
return -1;
}
++n->xproject;
}
}else if(n->ncp->x >= n->ncp->lenx){
++n->xproject;
}
// use ncplane_putegc on both planes because it'll get cursor movement right
if(ncplane_putegc(n->textarea, egc, NULL) < 0){
return -1;
}
if(ncplane_putegc(n->ncp, egc, NULL) < 0){
return -1;
}
if(n->textarea->x >= n->textarea->lenx - cols){
if(!n->horscroll){
n->textarea->x = n->textarea->lenx - cols;
}
}
if(n->ncp->x >= n->ncp->lenx - cols){
n->ncp->x = n->ncp->lenx - cols;
}
ncreader_redraw(n);
return 0;
}
static bool
do_backspace(ncreader* n){
int x = n->textarea->x;
int y = n->textarea->y;
if(n->textarea->x == 0){
if(n->textarea->y){
y = n->textarea->y - 1;
x = n->textarea->lenx - 1;
}
}else{
--x;
}
ncplane_putegc_yx(n->textarea, y, x, "", NULL);
ncplane_cursor_move_yx(n->textarea, y, x);
ncplane_cursor_move_yx(n->ncp, n->ncp->y, n->ncp->x - 1);
ncreader_redraw(n);
return true;
}
static bool
is_egc_wordbreak(ncplane* textarea){
char* egc = ncplane_at_yx(textarea, textarea->y, textarea->x, NULL, NULL);
if(egc == NULL){
return true;
}
wchar_t w;
mbstate_t mbstate;
memset(&mbstate, 0, sizeof(mbstate));
size_t s = mbrtowc(&w, egc, MB_CUR_MAX, &mbstate);
free(egc);
if(s == (size_t)-1 || s == (size_t)-2){
return true;
}
if(iswordbreak(w)){
return true;
}
return false;
}
static bool
ncreader_ctrl_input(ncreader* n, const ncinput* ni){
switch(ni->id){
case 'B':
ncreader_move_left(n);
break;
case 'F':
ncreader_move_right(n);
break;
case 'A': // cursor to beginning of line
while(n->textarea->x){
if(ncreader_move_left(n)){
break;
}
}
break;
case 'E': // cursor to end of line
while(n->textarea->x < ncplane_dim_x(n->textarea) - 1){
if(ncreader_move_right(n)){
break;
}
}
break;
case 'U': // clear line before cursor
while(n->textarea->x){
do_backspace(n);
}
break;
case 'W': // clear word before cursor
while(n->textarea->x){
if(ncreader_move_left(n)){
break;
}
if(is_egc_wordbreak(n->textarea)){
break;
}
if(ncreader_move_right(n)){
break;
}
do_backspace(n);
}
break;
default:
return false; // pass on all other ctrls
}
return true;
}
static bool
ncreader_alt_input(ncreader* n, const ncinput* ni){
switch(ni->id){
case 'b': // back one word (to first cell), but not to previous line
while(n->textarea->x){
if(ncreader_move_left(n)){
break;
}
if(is_egc_wordbreak(n->textarea)){
break;
}
}
break;
case 'f': // forward one word (past end cell)
while(n->textarea->x < ncplane_dim_x(n->textarea) - 1){
if(ncreader_move_right(n)){
break;
}
if(is_egc_wordbreak(n->textarea)){
break;
}
}
break;
default:
return false;
}
return true;
}
// we pass along:
// * anything with Alt
// * anything with Ctrl, except 'U' (which clears all input)
// * anything synthesized, save arrow keys and backspace
bool ncreader_offer_input(ncreader* n, const ncinput* ni){
if(ni->evtype == NCTYPE_RELEASE){
return false;
}
if(ncinput_ctrl_p(ni) && !n->no_cmd_keys){
return ncreader_ctrl_input(n, ni);
}else if(ncinput_alt_p(ni) && !n->no_cmd_keys){
return ncreader_alt_input(n, ni);
}
if(ncinput_alt_p(ni) || ncinput_ctrl_p(ni)){ // pass on all alts/ctrls if no_cmd_keys is set
return false;
}
if(ni->id == NCKEY_BACKSPACE){
return do_backspace(n);
}
// FIXME deal with multicolumn EGCs -- probably extract these and make them
// general ncplane_cursor_{left, right, up, down}()
if(ni->id == NCKEY_LEFT){
ncreader_move_left(n);
return true;
}else if(ni->id == NCKEY_RIGHT){
ncreader_move_right(n);
return true;
}else if(ni->id == NCKEY_UP){
ncreader_move_up(n);
return true;
}else if(ni->id == NCKEY_DOWN){
ncreader_move_down(n);
return true;
}else if(nckey_synthesized_p(ni->id)){
return false;
}
for (int c=0; ni->eff_text[c]!=0; c++){
unsigned char egc[5]={0};
if(notcurses_ucs32_to_utf8(&ni->eff_text[c], 1, egc, 4)>=0){
ncreader_write_egc(n, (char*)egc);
}
}
return true;
}
char* ncreader_contents(const ncreader* n){
return ncplane_contents(n->ncp, 0, 0, 0, 0);
}