#include #include #include #include #include /* defines */ #define CTRL(k) ((k) & 0x1f) // Control mask modifier #define TABSIZE 4 // Tab size as used in render #define STAT_SIZE 128 #define _XOPEN_SOURCE_EXTENDED #define EROW {0, NULL, 0, 0, NULL} /* main data structure containing: * -cursor position * +real (matching memory) * +offset (memory and screen offset) * +render (drawn on the screen) * -window size * -statusbar message */ struct term { struct { int x; int y; int off_x; int off_y; int r_x; int r_y; } cur; struct { int x; int y; } dim; char statusbar[STAT_SIZE]; int pad; } t; /* Row structure, defines actual and * render chars, actual and render size * and difference between render and * real size of the row * Utf-8 continuation chars */ typedef struct row { int size; char *chars; int r_size; int utf; char *render; } row; /* Rows structure (or file buffer) * defines rows and teh number of rows */ struct { row *rw; int rownum; } rows; /* Prototypes */ /* draw operations */ static void drawBar (char *s); static void drawScreen (); static void drawLines (void); static void updateRender (row *rw); static void curUpdateRender (void); static void cursorMove(int a); static int decimalSize (int n); static inline void lnMove (int y, int x); /* Row operations */ static inline void rowInit (void); static void rowAddChar (row *rw, char c, int pos); static void rowDeleteChar (row *rw, int select, int pos); static void rowCpy (row *to, row *from); static void rowAddRow (int pos); static void rowFree (row *rw); static void rowAddString (row *rw, char *s, int len, int pos); static void rowDeleteRow (int pos); /* Terminal operations */ static void termInit (void); static void termExit (void); static void termDie (char *s); /* file operations */ static void fileOpen (char *filename); void fileSave (char *filename); /* buffer operations */ static void rowAddLast (char *s, int len); /* garbage */ static void handleDel (int select); /* testing */ static void updateInfo (void); static int whatsThat (void); static inline int isUtf (int c); static inline int isCont (int c); static inline int isStart (int c); /* --------------------------------- main ------------------------------------ */ int main (int argc, char *argv[]) { /* Initialize the first row */ rowInit(); /* Try to open the file */ if (argc < 2) { perror("File not found"); exit(1); } else fileOpen(argv[1]); /* Initialize the terminal in raw mode, * start curses and initialize the term struct */ termInit(); /* Set the statusbar left (static) message */ snprintf(t.statusbar, STAT_SIZE, "%s %d lines %dx%d", argv[1], rows.rownum, t.dim.y, t.dim.x); /* remember the initial row number */ int irow = decimalSize(rows.rownum); /* Main event loop */ int c; while (1) { /* Redraw the screen */ drawScreen(); /* Wait for an event (keypress) */ switch (c = getch()) { case (CTRL('q')): termExit(); break; case (KEY_LEFT): case (KEY_RIGHT): case (KEY_UP): case (KEY_DOWN): cursorMove(c); break; case (KEY_BACKSPACE): handleDel(0); break; case (KEY_DC): handleDel(1); break; case (KEY_ENTER): case (10): case ('\r'): rowAddRow(t.cur.y); t.cur.y++; t.cur.x = 0; break; case (KEY_END): t.cur.y = rows.rownum - 1; break; case (KEY_HOME): t.cur.y = 0; break; default: if (c == KEY_STAB) c = '\t'; rowAddChar(&rows.rw[t.cur.y], c, t.cur.x); } if (decimalSize(rows.rownum) - irow) updateInfo(); } /* If by chance i find myself here be sure * end curses mode and clenaup */ termExit(); return 0; } /* ----------------------------- end of main ---------------------------------- */ void termInit (void) { /* Init locales */ setlocale(LC_ALL, ""); /* Init the screen and refresh */ initscr(); refresh(); /* Enable raw mode, this makes the termianl ignore * interrupt signals like CTRL+C and CTRL+Z * allowing us to make our own bindings */ raw(); /* Allow use of function keys */ keypad(stdscr, TRUE); /* Turn off echoing */ noecho(); /* Set the tab size */ set_tabsize(TABSIZE); /* Start color mode */ start_color(); init_pair(2, COLOR_BLACK, COLOR_CYAN); init_pair(1, COLOR_RED, COLOR_BLACK); /* Set default color */ //bkgd(COLOR_PAIR(1)); /* Populate the main data structure */ updateInfo(); /* Initialize the data staructure */ t.cur.x = t.cur.off_x = 0; t.cur.y = t.cur.off_y = 0; } /* Calculate the correct spacing for the line numbers * based on the size of the file */ int decimalSize (int n) { static int l; for (l = 0; n > 0; l++) n /= 10; return l + 1; } void termExit (void) { erase(); refresh(); endwin(); exit(0); } void termDie (char *s) { erase(); refresh(); endwin(); perror(s); exit(1); } /* ----------------------------- term operations -------------------------------- */ void drawScreen () { /* Clear the screen */ erase(); /* Update Scroll */ curUpdateRender(); /* draw the lines */ drawLines(); /* draw the bar */ drawBar(t.statusbar); /* move back to the cursor position */ lnMove(t.cur.r_y, t.cur.r_x); /* refresh the screen */ refresh(); } /* Draw all the appropriate lines (following cursor) to the screen */ void drawLines (void) { register int ln, i; static int start; for (i = 0, ln = 0; i < t.dim.y + t.cur.off_y; i++) { ln = i + t.cur.off_y; if (ln >= rows.rownum) break; /* Draw the line number */ attron(COLOR_PAIR(1)); mvprintw(i, 0, "%d", ln + 1); attroff(COLOR_PAIR(1)); lnMove(i, 0); /* Draw the line matcing render memory */ if (&rows.rw[ln] == NULL) termDie("drawlines NULL"); if (rows.rw[ln].r_size > t.cur.off_x) { start = t.cur.off_x; while (isCont(rows.rw[ln].render[start])) start++; addnstr(&rows.rw[ln].render[start], t.dim.x + 1 + rows.rw[ln].utf / 4); } lnMove(i + 1, 0); } lnMove(t.cur.y, t.cur.x); } /* Move avoiding the space allocated for line numbers */ inline void lnMove (int y, int x) { move(y, x + t.pad); } /* Draw the status bar at the bottom of the screen */ void drawBar (char *s) { /* Set maximum contrast for bar */ attron(COLOR_PAIR(2)); /* get the length of the statusbar text */ int len = strlen(s); /* Print the message */ mvprintw(t.dim.y, 0, s); /* Fill everything else with spaces */ static int i; for (i = len; i <= t.dim.x + t.pad; i++) mvaddch(t.dim.y, i, ' '); static char m[40]; sprintf(m, "x: %d y: %d Zoom: %c", t.cur.x, t.cur.y, whatsThat()); mvaddstr(t.dim.y, t.dim.x + t.pad - strlen(m), m); /* Return to normal contrast mode */ attroff(COLOR_PAIR(2)); } void updateRender (row *rw) { /* count the special characters * spaces, utf-8 continuation chars */ static int tabs, i, off, cc, ut; for (i = 0, tabs = 0, cc = 0, ut = 0; i < rw->size; i++) { if (rw->chars[i] == '\t') tabs++; else if (isCont(rw->chars[i])) cc++; else if (isUtf(rw->chars[i])) ut++; } rw->render = NULL; free(rw->render); /* Render is long as size with the added tab spaces - 1 * (we already count for the \t as a char) */ rw->render = malloc(rw->size + tabs * (TABSIZE - 1) + 1); if (rw->render == NULL) termDie ("malloc in updateRender"); /* put all the characters (substituing all special chars) * into the render buffer */ for (i = 0, off = 0; i < rw->size; i++) { if (rw->chars[i] == '\t') { for (int j = 0; j < TABSIZE; j++){ //if (!j) rw->render[off++] = '|'; //else rw->render[off++] = ' '; rw->render[off++] = ' '; } } else { rw->render[off++] = rw->chars[i]; } } rw->render[off] = '\0'; rw->r_size = off - cc; rw->utf = cc + ut; } /* -------------------------------- draw operations -------------------------------- */ /* Open a file and put it into a buffer line by line */ void fileOpen (char *filename) { FILE *fp = fopen(filename, "r"); if (fp == NULL) termDie("Cannot open file"); /* Init the linebuffer */ char *line = NULL; /* Set linecap to 0 so getline will atumatically allocate * memory for the line buffer*/ size_t linecap = 0; ssize_t linelen; /* getline returns -1 if no new line is present */ while ((linelen = getline(&line, &linecap, fp)) != -1) { while (linelen > 0 && (line[linelen - 1] == '\n' || line[linelen - 1] == '\r')) linelen--; rowAddLast(line, linelen); } /* free the line buffer */ free(line); /* close the file */ fclose(fp); } /*------------------------------------- file operations ----------------------------------*/ /* Add a row to the file buffer */ void rowAddLast (char *s, int len) { /* Extend the block of memory containing the lines */ row *newr = realloc(rows.rw, (rows.rownum + 1) * sizeof(row)); if (newr == NULL) termDie("realloc in rowAddLast"); else rows.rw = newr; /* Allocate memory for the line and copy it * at the current row number */ rows.rw[rows.rownum].chars = malloc(len + 1); if (rows.rw[rows.rownum].chars == NULL) termDie("malloc in rowAddLast chars"); memcpy(rows.rw[rows.rownum].chars, s, len); rows.rw[rows.rownum].chars[len] = '\0'; rows.rw[rows.rownum].size = len; updateRender(&rows.rw[rows.rownum]); rows.rownum++; } void rowInit (void) { rows.rw = NULL; rows.rownum = 0; } void rowAddChar (row *rw, char c, int pos) // WIP { // Error checking (allow tab) if (!c || (iscntrl(c) && c != '\t')) return; int i = 0; char *s = rw->chars; // reallocate mem and inc size rw->chars = malloc(rw->size + 2); if (rw->chars == NULL) termDie("malloc in rowAddchar"); rw->size++; // copy bf cursor for (i = 0; i < pos; i++) { rw->chars[i] = s[i]; } // add at cursor rw->chars[pos++] = c; //copy after cursor for (i = pos; i < rw->size + 1; i++) { rw->chars[i] = s[i - 1]; } free(s); updateRender(rw); t.cur.x++; } void rowDeleteChar (row *rw, int select, int pos) // WIP { //Do not delete NULL char if (rw->chars[pos - 1] == '\0' && pos) return; if (!pos && !select) return; if (rw->chars[pos] == '\0' && select) return; // Backspace if (!select) { for (int i = pos; i < rw->size + 1; i++) rw->chars[i - 1] = rw->chars[i]; t.cur.x--; // Delete } else { for (int i = pos; i < rw->size + 1; i++) rw->chars[i] = rw->chars[i + 1]; } char *s = realloc(rw->chars, rw->size); if (s != NULL) rw->chars = s; rw->size--; updateRender(rw); } void rowAddRow (int pos) // WIP; TO DOCUMENT { /* MOVE THE ROWS AWAY */ /* add another line to the bottom containing the previous * (last) line, effectively making new space */ rowAddLast(rows.rw[rows.rownum - 1].chars, rows.rw[rows.rownum - 1].size); /* copy all other lines until the specified position to the next one */ for (int last = rows.rownum - 1; last > pos; last--) rowCpy(&rows.rw[last], &rows.rw[last - 1]); /* SPLIT THE ROW AT POS AND STORE IT */ /* Get the row length at position after the cursor */ int len = rows.rw[pos].size - t.cur.x; /* create a dummy row as the new row souce */ row nex = EROW; /* allocate a buffer */ char *s = malloc(len + 1); if (s == NULL) termDie("malloc in rowAddRow s"); /* copy the contents of the pos row after the cursor into the buffer */ memcpy(s, &rows.rw[pos].chars[t.cur.x], len); s[len] = '\0'; /* update the dummy row */ nex.chars = s; nex.size = strlen(s); /* MAKE THE SPLIT INTO TWO LINES */ /* shrink the line at pos */ char *p = realloc(rows.rw[pos].chars, t.cur.x + 1); if (p == NULL) termDie("realloc in rowAddRow"); rows.rw[pos].chars = p; /* and terminate it with null like a good boi */ rows.rw[pos].chars[t.cur.x] = '\0'; /* update info and render */ rows.rw[pos].size = t.cur.x; updateRender(&rows.rw[pos]); /* copy the dummy to the new line and free */ rowCpy(&rows.rw[pos + 1], &nex); rowFree(&nex); } void rowFree (row *rw) // WIP { /* Free both render and memory strings */ free(rw->render); free(rw->chars); /* clear sizes */ rw->size = 0; rw->r_size = 0; rw->utf = 0; } void rowCpy (row *to, row *from) // WIP { /* Free the destination row (without destroying it) */ rowFree(to); /* Allocate space for the new string */ to->chars = (char*) malloc(strlen(from->chars) + 1); if (to->chars == NULL) termDie("malloc in rowCpy"); /* And update the size */ to->size = from->size; /* Then copy the chars from the source row to the destination row */ memcpy(to->chars, from->chars, to->size); to->chars[to->size] = '\0'; /* Then update the render */ updateRender(to); } void rowAddString (row *rw, char *s, int len, int pos) { char *temp = realloc(rw->chars, rw->size + len + 1); if (temp == NULL) termDie("realloc in rowAddString"); rw->chars = temp; if (pos == -1 || pos == rw->size) { memcpy(&rw->chars[rw->size], s, len); rw->size += len; rw->chars[rw->size] = '\0'; } else { memcpy(&rw->chars[rw->size], &rw->chars[rw->size - len], len); memcpy(&rw->chars[rw->size - len], s, len); rw->size += len; rw->chars[rw->size] = '\0'; } updateRender(rw); } void rowDeleteRow (int pos) { for (; pos < rows.rownum - 1; pos++) { rowCpy(&rows.rw[pos], &rows.rw[pos + 1]); // rowcpy already frees the row } rows.rownum--; rowFree(&rows.rw[rows.rownum]); row *temp = realloc(rows.rw, sizeof(row) * rows.rownum); if (temp == NULL) termDie("malloc in rowDeleteRow"); rows.rw = temp; } /* ----------------------------- row operations --------------------------- */ /* take care of the cursor movement */ void cursorMove (int a) { switch (a) { case (KEY_LEFT): if (t.cur.x <= 0) { if (t.cur.y) { t.cur.y--; t.cur.x = rows.rw[t.cur.y].size; } } else if (isCont(rows.rw[t.cur.y].chars[t.cur.x - 1])) { do { t.cur.x--; } while(!isStart(rows.rw[t.cur.y].chars[t.cur.x])); } else t.cur.x--; break; case (KEY_RIGHT): if (t.cur.x >= rows.rw[t.cur.y].size) { if (t.cur.y < rows.rownum - 1) { t.cur.y++; t.cur.x = 0; } } else if (isStart(rows.rw[t.cur.y].chars[t.cur.x])) { do { t.cur.x++; } while(isCont(rows.rw[t.cur.y].chars[t.cur.x])); } else t.cur.x++; break; case (KEY_UP): if (t.cur.y) { t.cur.y--; if (t.cur.x > rows.rw[t.cur.y].size) t.cur.x = rows.rw[t.cur.y].size; } break; case (KEY_DOWN): if (t.cur.y < rows.rownum - 1) { t.cur.y++; if (t.cur.x > rows.rw[t.cur.y].size) t.cur.x = rows.rw[t.cur.y].size; } break; } } void curUpdateRender () { // y if (t.cur.y >= t.cur.off_y && t.cur.y < t.cur.off_y + t.dim.y) { t.cur.r_y = t.cur.y - t.cur.off_y; } else if (t.cur.y >= t.cur.off_y + t.dim.y) { if (t.cur.y == t.cur.off_y + t.dim.y) t.cur.off_y++; else t.cur.off_y += t.cur.y - (t.cur.off_y + t.dim.y); t.cur.r_y = t.dim.y - 1; } else if (t.cur.y < t.cur.off_y) { t.cur.off_y -= t.cur.off_y - t.cur.y; t.cur.r_y = 0; } // x static int i, c; for (c = i = 0, t.cur.r_x = 0; i < t.cur.x; i++) { c = rows.rw[t.cur.y].chars[i]; /* continue (skip increment) if you encounter a continuation char */ if (isCont(c)) continue; else if (isStart(c)) t.cur.r_x++; if (c == '\t') t.cur.r_x += (TABSIZE - 1); t.cur.r_x++; } if (t.cur.r_x >= t.cur.off_x && t.cur.r_x < t.cur.off_x + t.dim.x) { t.cur.r_x -= t.cur.off_x; } else if (t.cur.r_x < t.cur.off_x) { t.cur.off_x -= t.cur.off_x - t.cur.r_x; t.cur.r_x = 0; } else if (t.cur.r_x >= t.cur.off_x + t.dim.x) { t.cur.off_x += t.cur.r_x - t.cur.off_x - t.dim.x; t.cur.r_x = t.dim.x; } } /*---------------------------------- scroll ------------------------------------*/ /* See whats under the cursor (memory) */ int whatsThat (void) { int c = rows.rw[t.cur.y].chars[t.cur.x]; switch (c) { case ('\t'): return '^'; break; case (' '): return '~'; break; case ('\0'): return '.'; break; default: return c; break; } return 0; } void handleDel (int select) { if (!select) { if (t.cur.x <= 0 && t.cur.y > 0) { t.cur.x = rows.rw[t.cur.y - 1].size; rowAddString(&rows.rw[t.cur.y - 1], rows.rw[t.cur.y].chars, rows.rw[t.cur.y].size, -1); rowDeleteRow(t.cur.y); t.cur.y--; } else { rowDeleteChar(&rows.rw[t.cur.y], 0, t.cur.x); } } else { if (t.cur.x >= rows.rw[t.cur.y].size) { rowAddString(&rows.rw[t.cur.y], rows.rw[t.cur.y + 1].chars, rows.rw[t.cur.y + 1].size, -1); rowDeleteRow(t.cur.y + 1); } else { rowDeleteChar(&rows.rw[t.cur.y], 1, t.cur.x); } } } void updateInfo (void) { getmaxyx(stdscr, t.dim.y, t.dim.x); t.dim.y -= 1; t.pad = decimalSize(rows.rownum - 1); t.dim.x -= t.pad + 1; } int isUtf (int c) { return (c >= 0x80 || c < 0 ? 1 : 0); } int isCont (int c) { return ((c &= 0xC0) == 0x80 ? 1 : 0); } int isStart (int c) { return (isUtf(c) && !isCont(c) ? 1 : 0); } /*--------------------------------- testing ------------------------------------*/