Ncurses Multi-Line Text Editor for Data Entry Applications
Including word wrap, scrolling and four different options for handling carriage return
By Frank Cox
(July 21,
2013)
This is a multi-line text editor that's intended to be incorporated into programs that require data entry. If you need a single-line data entry function instead, you may be interested in my Editable Single Line Input Field for ncurses, which can be found here.
This multi-line text editor allows you to determine the maximum number of characters to allow in the input string and the maximum number of lines of text that are allowed, as well as the location and size of the text entry window so you can have several input fields on the screen at one time if desired. You can allow all of the regular characters -- letters, numbers and punctuation -- or restrict the input to a certain subset of characters, by allowing only the characters that are required for your program.
If the maximum number of lines of text exceeds the height of the text entry window, scrolling happens automatically as required. Note that this function does vertical scrolling only; horizontal scrolling is not implemented.
Four different options are provided for handling a carriage return (the user hitting the enter key). Upon receipt of a carriage return you can use the normal behavior and move to the next input line, allowing the user to create paragraphs. You can ignore the carriage return completely, or you can convert all carriage returns to spaces. This last behaviour is useful when entering data using cut-and-paste where the data to be entered is on short lines with a carriage return at the end of each line and what you really want is a single paragraph of input.
The last option for carriage return handling is to use that to exit the editor function.
Other ways to exit the editor function are to press function key F1 or hit the Escape key twice. Either of these actions will discard all changes that the user has made in the input data and restore the text string to what it was originally. (Due to the way that ncurses works, hitting the Escape key only once will be ignored.)
Hitting the Tab key, Shift-Tab (or the Enter key if you have selected that option) will exit the function and retain all of the user's changes in the text string.
Each of the above methods for exiting the editor function provides a different return code so you can have your program do things like use Tab to move to the next field and Shift-Tab to move to the previous field, etc.
Since this is a data entry text editor, moving around within the text is pretty straight-forward. The cursor keys (up,down,left, right) work as expected, Home takes you to the first character on the current line, End takes you to the last character on the current line, PageUp takes you to the first character in the text string, and PageDown takes you to the last character in the text string.
Function key F3 erases the entire text string.
Control-Y erases the current line.
This program was written on Centos 6 and requires ncurses, which is installed by default on most Linux distributions. You should be able to compile it as-is on pretty much any modern Linux or Unix system. I really don't know what it would take to get this stuff working on Microsoft Windows, though there does appear to be a MS Windows compatible version of ncurses called pdcurses which might work.
You can obtain the source code by cutting-and pasting what you see here, or by clicking this link.
/* multiline-text-editor.c Copyright (c) 2013, Frank Cox All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY FRANK COX ''AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL FRANK COX BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include <ncurses.h> #include <string.h> #include <stdlib.h> char* left(char *string, const int length); void malloc_error(void); int texteditor(char *text,int maxchars,const int startrow, const int startcol,int maxrows,const int maxcols,const int displayrows, const int returnhandler,const char *permitted, bool ins, const bool allowcr); int main(void) { int maxchars=40*80+1,startrow=0,startcol=0,maxrows=40,maxcols=80,displayrows=20,returnhandler=1; char *message; bool ins=false,allowcr=true; initscr(); noecho(); cbreak(); keypad(stdscr,TRUE); move(20,0); hline(ACS_CKBOARD,80); mvaddstr(21,0,"Press TAB or BACK TAB to move between fields"); mvaddstr(21,50,"Press F3 to Clear Text"); mvaddstr(22,0,"Press CTRL-Y to Delete Current Line"); message=malloc(maxchars*sizeof(char)); strcpy(message,"Multi-line Text Editor Demo"); texteditor(message,maxchars,startrow,startcol,maxrows,maxcols,displayrows,returnhandler,NULL,ins,allowcr); clear(); addstr("Exited text editor"); getch(); endwin(); printf("Message\n-------\n\"%s\"",message); free(message); return 0; } int texteditor(char *text, int maxchars, const int startrow, const int startcol,int maxrows,const int maxcols,const int displayrows,const int returnhandler,const char *permitted, bool ins, const bool allowcr) /* * PARAMETERS * ---------- * char * text = text string to edit * const int maxchars = maximum number of characters to allow in text string, including final \0 (example: string "ABC"=maxchars 4) * NOTE: If maxchars==0 then maxchars=maxrows*maxcols+1 * const int startrow = top row for the text entry window * const int startcol = top column number for the text entry window * int maxrows = maximum number of rows (lines) to allow in the text string. * NOTE: If maxrows > displayrows, the text entry window will scroll vertically * If maxrows == displayrows, text entry window scrolling is disabled * If maxrows ==0 then maxrows=maxchars-1, i.e one character per line * const int maxcols = width of the text entry window * const int displayrows = number of rows in the text entry window, if < maxrows the text entry window will scroll * NOTE: A row does not necessarily end with \n, it may end with a space * or simply wrap to the next row if the continuous length of the word exceeds maxcols * const int returnhandler = 0 ignore cr * 1 cr=\n * 2 replace cr with ' ' * 3 exit function when cr is received * const char *permitted = NULL all regular text characters accepted * if a string, accept only characters included in the string * bool ins = initial insert mode on/off (user changable by hitting the ins key while editing) * const bool allowcr = allow a \n as the first character in the string, i.e. a blank line at the top of the entered string * * EDITING CONTROL KEYS * INS = insert mode on/off * PgUp = goto first character in text string * PgDn = goto last character in text string * Home = goto first character on current row (line) * End = goto last character on current row (line) * ctrl-Y = delete current line * F3 = delete all text * F1 = exit function and discard all changes to the text * ESC (twice) = same as F1 * tab = exit function * back tab (shift-tab) = exit function * return = exit function if returnhandler==3 * * RETURN VALUE * ------------ * Describes the key that was used to exit the function * tab=6 * back tab (shift-tab) = 4 * F1 or ESC (twice)= 27 * return =0 (if returnhandler==3) */ { int ky,position=0,row,col,scrollstart=0; char *where, *original, *savetext,**display; bool exitflag=false; if (!maxchars) maxchars=maxrows*maxcols+1; if (!maxrows || maxrows > maxchars-1) maxrows=maxchars-1; if (ins) curs_set(2); else curs_set(1); if ((display = malloc(maxrows * sizeof(char *))) ==NULL) malloc_error(); for(ky = 0; ky < maxrows; ky++) if ((display[ky] = malloc((maxcols+1) * sizeof(char)))==NULL) malloc_error(); if ((original=malloc(maxchars*sizeof(char)))==NULL) malloc_error(); strcpy(original,text); if ((savetext=malloc(maxchars*sizeof(char)))==NULL) malloc_error(); while (!exitflag) { int counter; do { counter=0; where=text; for (row=0; row < maxrows; row++) { display[row][0]=127; display[row][1]='\0'; } row=0; while ((strlen(where) > maxcols || strchr(where,'\n') != NULL) && (display[maxrows-1][0]==127 || display[maxrows-1][0]=='\n')) { strncpy(display[row],where,maxcols); display[row][maxcols]='\0'; if (strchr(display[row],'\n') != NULL) left(display[row],strchr(display[row],'\n')-display[row]); else left(display[row],strrchr(display[row],' ')-display[row]); if (display[maxrows-1][0]==127 || display[maxrows-1][0]=='\n') { where+=strlen(display[row]); if (where[0]=='\n' || where[0]==' ' || where[0]=='\0') where++; row++; } } if (row == maxrows-1 && strlen(where) > maxcols) // undo last edit because wordwrap would make maxrows-1 longer than maxcols { strcpy(text,savetext); if (ky==KEY_BACKSPACE) position++; counter=1; } } while (counter); strcpy(display[row],where); ky=0; if (strchr(display[maxrows-1],'\n') != NULL) // if we have a \n in the last line then we check what the next character is to insure that we don't write stuff into the last line+1 if (strchr(display[maxrows-1],'\n')[1] != '\0') ky=KEY_BACKSPACE; // delete the last character we entered if it would push the text into the last line +1 col=position; row=0; counter=0; while (col > strlen(display[row])) { col-=strlen(display[row]); counter+=strlen(display[row]); if (text[counter] ==' ' || text[counter]=='\n' || text[counter]=='\0') { col--; counter++; } row++; } if (col > maxcols-1) { row++; col=0; } if (!ky) // otherwise ky==KEY_BACKSPACE and we're getting rid of the last character we entered to avoid pushing text into the last line +1 { if (row < scrollstart) scrollstart--; if (row > scrollstart+displayrows-1) scrollstart++; for (counter=0;counter < displayrows; counter++) { mvhline(counter+startrow,startcol,' ',maxcols); if (display[counter+scrollstart][0] != 127) mvprintw(counter+startrow,startcol,"%s",display[counter+scrollstart]); } move(row+startrow-scrollstart,col+startcol); ky=getch(); } switch(ky) { case KEY_F(1): // function key 1 strcpy(text,original); case 9: // tab case KEY_BTAB: // shift-tab exitflag=true; break; case 27: //esc // esc twice to get out, otherwise eat the chars that don't work //from home or end on the keypad ky=getch(); if (ky == 27) { strcpy(text,original); exitflag=true; } else if (ky=='[') { getch(); getch(); } else ungetch(ky); break; case KEY_F(3): memset(text,'\0',maxchars); position=0; scrollstart=0; break; case KEY_HOME: if (col) { position=0; for (col=0; col < row; col++) { position += strlen(display[col]); if ((strchr(display[row],'\n') != NULL) || (strchr(display[row],' ') != NULL)) position++; } } break; case KEY_END: if (col < strlen(display[row])) { position=-1; for (col=0; col <=row; col++) { position+=strlen(display[col]); if ((strchr(display[row],'\n') != NULL) || (strchr(display[row],' ') != NULL)) position++; } } break; case KEY_PPAGE: position=0; scrollstart=0; break; case KEY_NPAGE: position=strlen(text); for (counter=0; counter < maxrows; counter++) if(display[counter][0]==127) break; scrollstart=counter-displayrows; if (scrollstart < 0) scrollstart=0; break; case KEY_LEFT: if (position) position--; break; case KEY_RIGHT: if (position < strlen(text) && (row != maxrows-1 || col < maxcols-1)) position++; break; case KEY_UP: if (row) { if (col > strlen(display[row-1])) position=strlen(display[row-1]); else position=col; ky=0; for (col=0; col < row-1; col++) { position+=strlen(display[col]); ky+=strlen(display[col]); if ((strlen(display[col]) < maxcols) || (text[ky]==' ' && strlen(display[col])==maxcols)) { position++; ky++; } } } break; case KEY_DOWN: if (row < maxrows-1) if (display[row+1][0] !=127) { if (col < strlen(display[row+1])) position=col; else position=strlen(display[row+1]); ky=0; for (col=0; col <= row; col++) { position+=strlen(display[col]); ky+=strlen(display[col]); if ((strlen(display[col]) < maxcols) || (text[ky]==' ' && strlen(display[col])==maxcols)) { position++; ky++; } } } break; case KEY_IC: // insert key ins=!ins; if (ins) curs_set(2); else curs_set(1); break; case KEY_DC: // delete key if (strlen(text)) { strcpy(savetext,text); memmove(&text[position],&text[position+1],maxchars-position); } break; case KEY_BACKSPACE: if (strlen(text) && position) { strcpy(savetext,text); position--; memmove(&text[position],&text[position+1],maxchars-position); } break; case 25: // ctrl-y if (display[1][0] != 127) { position-=col; ky=0; do { memmove(&text[position],&text[position+1],maxchars-position); ky++; } while (ky < strlen(display[row])); } else memset(text,'\0',maxchars); break; case 10: // return switch (returnhandler) { case 1: if (display[maxrows-1][0] == 127 || display[maxrows-1][0] == '\n') { memmove(&text[position+1],&text[position],maxchars-position); text[position]='\n'; position++; } break; case 2: ky=' '; ungetch(ky); break; case 3: exitflag=true; } break; default: if (((permitted==NULL && ky > 31 && ky < 127) || (permitted != NULL && strchr(permitted,ky))) && strlen(text) < maxchars-1 && (row !=maxrows-1 || (strlen(display[maxrows-1]) < maxcols || (ins && (row!=maxrows-1 && col < maxcols ))))) { if (ins || text[position+1]=='\n' || text[position]== '\n') memmove(&text[position+1],&text[position],maxchars-position); text[position]=ky; if (row != maxrows-1 || col < maxcols-1) position++; } } if(!allowcr) if (text[0]=='\n') { memmove(&text[0],&text[1],maxchars-1); if (position) position--; } } free(original); free(savetext); for(scrollstart = 0; scrollstart < maxrows; scrollstart++) free(display[scrollstart]); free(display); switch(ky) { case 9: // tab return 6; case KEY_BTAB: return 4; case KEY_F(1): case 27: // esc return 5; } return 0; // we hit the return key and returnhandler=3 } char* left(char *string, const int length) { if (strlen(string) > length) string[length]='\0'; return string; } void malloc_error(void) { endwin(); fprintf(stderr, "malloc error:out of memory\n"); exit(EXIT_FAILURE); }
Other articles written by Frank Cox can be found here.
Frank Cox owns and operates the Melville Theatre in Melville, Saskatchewan, Canada, and has been playing with computers for over 30 years.
This work is licensed under a Creative
Commons Attribution-Share Alike 2.5 Canada License.