Motivation
This article delves into the intricate process of conceptualizing and constructing an ultra-minimalist, console-based code editor. The primary objective is to create a tool that is exceptionally small, highly portable. This editor will focus solely on the fundamental task of text manipulation, making it ideal for environments where resources are scarce or a pure, distraction-free coding experience is desired.
The philosophy underpinning this endeavor is one of extreme minimalism. Every design choice and every line of code will be scrutinized for its necessity. The editor's footprint, both in terms of executable size and runtime memory consumption, will be kept to an absolute minimum.
Portability means it should run directly from a USB drive or any directory without requiring installation or leaving behind configuration files in system-specific locations. This approach ensures that the editor is a true "pocket-sized" development tool.
Interfacing with the Terminal - The Foundation
The very first step in building a console-based editor involves establishing a direct and unbuffered communication channel with the terminal. Unlike standard input/output operations where the operating system often buffers characters and processes special keys (like Enter or Backspace), a code editor requires immediate, raw access to every key press. This "raw mode" allows the editor to interpret individual keystrokes, including arrow keys and control combinations, without the terminal itself trying to process them.
On Unix-like operating systems (such as Linux or macOS), this is typically achieved by manipulating the termios structure. This structure contains various flags that control terminal behavior. By disabling canonical mode and echo, among other settings, we can put the terminal into a state where it simply passes raw character data directly to our program. Conversely, when the editor exits, it is crucial to restore the terminal to its original, canonical mode to ensure normal shell operation.
Here is a small C code snippet illustrating how to enter and exit raw mode on Unix-like systems:
#include <termios.h> // Required for termios functions
#include <unistd.h> // Required for STDIN_FILENO
// Global variable to store original terminal attributes
static struct termios original_termios;
// Function to disable canonical mode and echo
void enableRawMode() {
// Get the current terminal attributes
tcgetattr(STDIN_FILENO, &original_termios);
// Create a copy to modify
struct termios raw = original_termios;
// Disable canonical mode (line-by-line input)
// Disable echo (don't print characters typed by user)
// Disable ISIG (don't send SIGINT/SIGTSTP on Ctrl-C/Ctrl-Z)
// Disable ICANON (disable canonical mode)
// Disable IEXTEN (disable Ctrl-V, Ctrl-O)
// Disable ICRNL (disable Ctrl-M to newline conversion)
raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);
// Disable output post-processing (e.g., \n to \r\n conversion)
raw.c_oflag &= ~(OPOST);
// Set minimum number of characters for non-canonical read
raw.c_cc[VMIN] = 0;
// Set timeout for read in tenths of a second
raw.c_cc[VTIME] = 1;
// Apply the new attributes immediately
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}
// Function to restore original terminal mode
void disableRawMode() {
// Restore the terminal attributes to their original state
tcsetattr(STDIN_FILENO, TCSAFLUSH, &original_termios);
}
After configuring raw input, the next crucial aspect is controlling the terminal's output. This involves positioning the cursor, clearing parts of the screen, and changing text colors. These operations are typically performed using ANSI escape codes, which are special sequences of characters that the terminal interprets as commands rather than printable text. For instance, \x1b[H moves the cursor to the top-left corner, \x1b[2J clears the entire screen, and \x1b[<row>;<col>H moves the cursor to a specific row and column.
Here is a small C code snippet demonstrating basic ANSI escape code usage for cursor positioning and screen clearing:
#include <stdio.h> // Required for printf
// Function to clear the entire terminal screen
void clearScreen() {
// ANSI escape code to clear the entire screen
printf("\x1b[2J");
}
// Function to move the cursor to a specific row and column
// Row and column are 1-based
void moveCursor(int row, int col) {
// ANSI escape code to set cursor position
printf("\x1b[%d;%dH", row, col);
}
// Function to hide the cursor
void hideCursor() {
// ANSI escape code to hide the cursor
printf("\x1b[?25l");
}
// Function to show the cursor
void showCursor() {
// ANSI escape code to show the cursor
printf("\x1b[?25h");
}
These functions form the bedrock of any console-based interactive application, allowing the editor to precisely control what the user sees and how input is received.
Here is a simple ASCII representation of a terminal screen with a cursor, illustrating the conceptual layout:
+-----------------------------------------+
| Line 1: This is some text. |
| Line 2: Another line of text. |
| Line 3: The cursor is currently here >_ |
| Line 4: |
| Line 5: |
| ... |
| Line N: |
+-----------------------------------------+
Managing the Document - The Text Buffer
The core data structure of any code editor is its text buffer, which holds the actual content of the file being edited. For a minimalist editor, a simple yet effective approach is to store the document as an array of dynamically allocated strings, where each string represents a single line of text. This line-based storage simplifies many common editor operations, such as inserting new lines, deleting entire lines, and navigating vertically.
To ensure efficiency and flexibility, the text buffer itself, as well as individual lines, should be dynamically resizable. When a new line is added or a character is inserted into a line, memory can be reallocated as needed. This avoids imposing arbitrary limits on file size or line length, while still allowing for a compact representation when files are small.
Consider a C structure to represent a single line of text within our editor:
#include <stdlib.h> // Required for malloc, realloc, free
// Structure to represent a single line of text
typedef struct {
char *chars; // Pointer to the character data of the line
int length; // Current number of characters in the line
int capacity; // Allocated memory capacity for the line
} Line;
// Function to initialize a new line structure
void initLine(Line *line) {
line->chars = NULL;
line->length = 0;
line->capacity = 0;
}
// Function to free memory associated with a line
void freeLine(Line *line) {
if (line->chars != NULL) {
free(line->chars);
line->chars = NULL;
}
line->length = 0;
line->capacity = 0;
}
The editor would then manage an array of these Line structures. When a character is added to a line, the chars array within that Line structure might need to be reallocated if its length approaches its capacity.
Here is a function signature for adding a character to a line, demonstrating the dynamic nature:
#include <stdlib.h> // Required for realloc
#include <string.h> // Required for memmove
// Define a reasonable initial capacity for a line
#define LINE_INITIAL_CAPACITY 16
// Function to insert a character into a line at a specific position
// Returns 1 on success, 0 on memory allocation failure
int insertCharIntoLine(Line *line, int at, int c) {
// Check if reallocation is needed
if (line->length + 1 > line->capacity) {
int newCapacity = line->capacity == 0 ? LINE_INITIAL_CAPACITY : line->capacity * 2;
char *newChars = (char *)realloc(line->chars, newCapacity);
if (newChars == NULL) {
return 0; // Memory allocation failed
}
line->chars = newChars;
line->capacity = newCapacity;
}
// Shift characters to the right to make space for the new character
// memmove handles overlapping memory regions correctly
memmove(&line->chars[at + 1], &line->chars[at], line->length - at);
// Insert the new character
line->chars[at] = (char)c;
line->length++;
return 1; // Success
}
This dynamic allocation strategy ensures that the editor adapts to the content without wasting excessive memory for empty space or limiting the user's input.
Rendering the View - Displaying Content
With the terminal interaction layer and the text buffer in place, the next challenge is to render the document's content onto the screen. This involves iterating through the lines in the text buffer, drawing the visible portion of each line, and carefully positioning the editor's cursor. Since the terminal screen has a fixed number of rows and columns, the editor must manage a "viewport" into the larger document. This means keeping track of a vertical offset, indicating which line of the document is currently displayed at the top of the screen.
When rendering, the editor first clears the screen to remove any previous content. Then, for each visible row on the terminal, it determines which line from the text buffer corresponds to that row, taking into account the vertical scroll offset. It then prints that line, ensuring that it does not exceed the terminal's width. If a line is longer than the screen width, it can either be truncated or wrapped, though for simplicity in a minimalist editor, truncation is often preferred. Finally, after all visible text is drawn, the editor positions the terminal's hardware cursor to match the editor's internal cursor position, making it appear as if the user is typing directly into the text.
Here is a simplified C function for drawing a line, demonstrating how to handle screen width:
#include <stdio.h> // Required for printf
#include <string.h> // Required for strlen
// Assume a global struct for editor state, including screen dimensions
// For example:
// typedef struct {
// int screenRows;
// int screenCols;
// int currentRow; // Editor's internal cursor row
// int currentCol; // Editor's internal cursor column
// int rowOffset; // Vertical scroll offset
// Line *lines; // Array of Line structures
// int numLines; // Total number of lines in the buffer
// } EditorState;
// extern EditorState E; // Global editor state
// Function to draw a single line of text to the screen
// 'line' is the Line structure to draw
// 'screenRow' is the 0-based row on the terminal where the line should appear
// 'screenCols' is the total width of the terminal screen
void drawLine(const Line *line, int screenRow, int screenCols) {
// Move cursor to the start of the current screen row
moveCursor(screenRow + 1, 1); // +1 because ANSI is 1-based
// Clear the current line on the screen
printf("\x1b[K"); // ANSI escape code to clear from cursor to end of line
// Determine the portion of the line to display
int len_to_print = line->length;
if (len_to_print > screenCols) {
len_to_print = screenCols; // Truncate if line is too long
}
// Print the visible part of the line
if (line->chars != NULL && len_to_print > 0) {
printf("%.*s", len_to_print, line->chars);
}
}
// Function to refresh the entire screen content
// This function would be called after any change to the buffer or cursor position
void refreshScreen(EditorState *E) {
hideCursor(); // Hide cursor during redraw to prevent flickering
clearScreen(); // Clear the entire screen
// Iterate through visible screen rows
for (int y = 0; y < E->screenRows; y++) {
int fileRow = y + E->rowOffset; // Calculate corresponding file row
if (fileRow < E->numLines) {
// Draw the actual line from the buffer
drawLine(&E->lines[fileRow], y, E->screenCols);
} else if (fileRow == E->numLines) {
// Draw a tilde for empty lines below the file content
moveCursor(y + 1, 1);
printf("~");
}
}
// Position the cursor at the editor's internal cursor position
moveCursor(E->currentRow - E->rowOffset + 1, E->currentCol + 1);
showCursor(); // Show cursor after redraw
}
This rendering process ensures that the user always sees an up-to-date representation of their document, with the cursor accurately reflecting their editing position.
Here is an ASCII representation of the editor's view, showing text and the cursor:
+----------------------------------------------------------------+
| This is the first line of code. |
| This is the second line. |
| This is the third line. |
| ~ ^ |
| ~ |
| ~ |
| |
| --- STATUS --- File: example.c (Modified) --- Line 3, Col 15 |
+----------------------------------------------------------------+
In this figure, ^ represents the cursor's position. The ~ characters indicate lines beyond the file's content, a common convention in console editors. The bottom line provides a minimalist status bar.
Processing Input - Bringing it to Life
The input processing layer is the brain of the editor, responsible for translating raw key presses into meaningful editor commands. After putting the terminal into raw mode, the editor continuously reads individual characters or escape sequences. Each input is then evaluated, and a corresponding action is triggered. This involves a large switch statement or a series of if-else conditions to handle various keys.
Common key presses include printable characters (which are inserted into the text buffer), arrow keys (for cursor movement), Enter (to insert a new line), Backspace (to delete the character before the cursor), and control combinations (like Ctrl+S for saving or Ctrl+Q for quitting). The challenge lies in distinguishing between single characters and multi-byte escape sequences, which are often used for special keys like arrow keys or function keys.
Here is a C code snippet for reading a single character in raw mode:
#include <unistd.h> // Required for read
// Function to read a single character from stdin in raw mode
// Returns the character read, or -1 on error/no input within timeout
int readKey() {
char c;
ssize_t bytesRead = read(STDIN_FILENO, &c, 1);
if (bytesRead == -1) {
// Handle error, e.g., print a message or exit
// For a robust editor, proper error handling is crucial
return -1;
}
return (int)c;
}
Once a key is read, the editor needs to decide what to do with it. This usually involves a dispatch mechanism, such as a switch statement, to handle different key codes. Special keys often come as multi-byte escape sequences, requiring the editor to read additional bytes to fully identify them.
Here is a simplified switch statement structure for handling various key presses:
#include <stdio.h> // Required for printf
#include <stdlib.h> // Required for exit
// Define key codes for common special keys
// These are often derived from ANSI escape sequences
#define ARROW_LEFT 1000
#define ARROW_RIGHT 1001
#define ARROW_UP 1002
#define ARROW_DOWN 1003
#define PAGE_UP 1004
#define PAGE_DOWN 1005
#define HOME_KEY 1006
#define END_KEY 1007
#define DELETE_KEY 1008
// Function to process a key press
void processKeyPress(EditorState *E) {
int c = readKey(); // Read a character/key code
switch (c) {
case 'q': // Quit command (e.g., Ctrl+Q)
// For a minimalist editor, 'q' might be enough, or 'Ctrl+q'
// For Ctrl+Q, you would check for specific control character values
// (e.g., ASCII 17 for Ctrl-Q)
printf("Quitting editor...\n");
exit(0);
break;
case 's': // Save command (e.g., Ctrl+S)
// For Ctrl+S, you would check for specific control character values
// (e.g., ASCII 19 for Ctrl-S)
// saveFile(E->filename, E->lines, E->numLines);
printf("File saved (conceptually).\n");
break;
case '\r': // Enter key (carriage return)
// insertNewline(E);
printf("Newline inserted (conceptually).\n");
break;
case 127: // Backspace key (ASCII 127)
// deleteChar(E);
printf("Character deleted (conceptually).\n");
break;
case ARROW_UP:
// moveCursorUp(E);
printf("Cursor moved up (conceptually).\n");
break;
case ARROW_DOWN:
// moveCursorDown(E);
printf("Cursor moved down (conceptually).\n");
break;
case ARROW_LEFT:
// moveCursorLeft(E);
printf("Cursor moved left (conceptually).\n");
break;
case ARROW_RIGHT:
// moveCursorRight(E);
printf("Cursor moved right (conceptually).\n");
break;
default:
// If it's a printable character, insert it into the buffer
if (c >= 32 && c <= 126) { // ASCII printable characters
// insertChar(E, c);
printf("Character '%c' inserted (conceptually).\n", c);
}
break;
}
}
This input processing loop continuously reads keys, updates the editor's internal state (cursor position, text buffer), and then triggers a screen refresh to show the changes to the user.
File Operations - Persistence
No code editor is complete without the ability to load and save files. These operations connect the in-memory text buffer to the persistent storage of the file system. When loading a file, the editor reads its content line by line, dynamically allocating memory and populating its text buffer. When saving, the editor writes the entire content of its text buffer back to a file.
Robust file operations require careful error handling. The editor must gracefully handle scenarios such as files not existing, permission issues, or disk full errors. For a minimalist editor, standard C library functions like fopen, fgets, fputs, and fclose are perfectly adequate and highly portable.
Here is a C function signature for loading a file:
#include <stdio.h> // Required for FILE, fopen, fclose, fgets
#include <stdlib.h> // Required for malloc, free
#include <string.h> // Required for strlen, strdup
// Function to load a file into the editor's buffer
// Returns 1 on success, 0 on failure
int loadFile(const char *filename, Line **lines, int *numLines) {
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
// Handle file open error, e.g., file not found
*lines = NULL;
*numLines = 0;
return 0;
}
// Initial allocation for lines array
int currentCapacity = 10;
*lines = (Line *)malloc(sizeof(Line) * currentCapacity);
if (*lines == NULL) {
fclose(fp);
return 0; // Memory allocation failed
}
*numLines = 0;
char *lineBuffer = NULL;
size_t bufferSize = 0;
ssize_t readLength;
while ((readLength = getline(&lineBuffer, &bufferSize, fp)) != -1) {
// Remove trailing newline character if present
if (readLength > 0 && lineBuffer[readLength - 1] == '\n') {
lineBuffer[readLength - 1] = '\0';
readLength--;
}
// Ensure enough space in the lines array
if (*numLines >= currentCapacity) {
currentCapacity *= 2;
Line *newLines = (Line *)realloc(*lines, sizeof(Line) * currentCapacity);
if (newLines == NULL) {
free(lineBuffer);
// Free already allocated lines before returning
for (int i = 0; i < *numLines; i++) {
freeLine(&(*lines)[i]);
}
free(*lines);
*lines = NULL;
*numLines = 0;
fclose(fp);
return 0; // Memory allocation failed
}
*lines = newLines;
}
// Initialize and populate the new line
initLine(&(*lines)[*numLines]);
if (insertCharIntoLine(&(*lines)[*numLines], 0, '\0') == 0) { // Allocate initial capacity
// Handle error, free resources, etc.
free(lineBuffer);
fclose(fp);
return 0;
}
// Copy content from lineBuffer to the Line structure
for (int i = 0; i < readLength; i++) {
if (insertCharIntoLine(&(*lines)[*numLines], i, lineBuffer[i]) == 0) {
free(lineBuffer);
fclose(fp);
return 0;
}
}
(*numLines)++;
}
free(lineBuffer); // Free the buffer used by getline
fclose(fp);
return 1; // Success
}
Note: The getline function is a GNU extension. For wider portability, one might implement a custom get_line function using fgetc or fgets with dynamic resizing. The running example will use a robust, portable implementation.
Here is a C function signature for saving a file:
#include <stdio.h> // Required for FILE, fopen, fclose, fputs
// Function to save the editor's buffer to a file
// Returns 1 on success, 0 on failure
int saveFile(const char *filename, const Line *lines, int numLines) {
FILE *fp = fopen(filename, "w"); // Open in write mode, truncates if exists
if (fp == NULL) {
// Handle file open error, e.g., permission denied
return 0;
}
for (int i = 0; i < numLines; i++) {
if (lines[i].chars != NULL) {
if (fputs(lines[i].chars, fp) == EOF) {
fclose(fp);
return 0; // Error writing to file
}
}
if (fputc('\n', fp) == EOF) { // Write newline character after each line
fclose(fp);
return 0; // Error writing newline
}
}
fclose(fp);
return 1; // Success
}
These functions provide the essential persistence mechanisms, allowing users to load existing code and save their changes.
Portability and Cross-Platform Considerations
Achieving true portability for a console-based editor, especially one written in C, largely revolves around abstracting operating system-specific functionalities. The most significant divergence between Unix-like systems and Windows lies in terminal input/output.
On Unix-like systems, the termios API (as demonstrated earlier) is used to control terminal modes. For reading single characters, read(STDIN_FILENO, ...) is employed. On Windows, the console API provides equivalent but distinct functions, such as GetConsoleMode, SetConsoleMode, and ReadConsoleInput. These functions interact with the console buffer and input events rather than a termios structure.
To maintain a single codebase, conditional compilation using preprocessor directives like #ifdef _WIN32is invaluable. This allows different blocks of code to be compiled depending on the target operating system. For example, the enableRawMode and disableRawMode functions would have distinct implementations for Unix and Windows, but their public interfaces would remain the same, abstracting the underlying OS details from the rest of the editor logic.
For file I/O, the standard C library functions (fopen, fclose, fread, fwrite, fputs, fgets, fgetc, fputc) are highly portable across all major operating systems. Adhering to these standard functions minimizes platform-specific dependencies. Similarly, memory management functions (malloc, realloc, free) are universally available. The goal is to isolate platform-specific code into a minimal set of helper functions, making the core editor logic entirely platform-agnostic.
Clean Code in a Minimalist Context
Even in the pursuit of minimalism, adhering to clean code and clean architecture principles is paramount. A minimalist editor should not be synonymous with spaghetti code. In fact, due to its constrained nature, well-structured and readable code becomes even more critical for maintainability and correctness.
Modularity is a key principle. Despite its small size, the editor's functionality can be logically divided into distinct modules or functions. For example, terminal interaction can be encapsulated in one set of functions, text buffer management in another, rendering in a third, and input processing in a fourth. This separation of concerns makes each part of the code easier to understand, test, and debug independently.
Clear variable names and comprehensive comments are essential. Even if the code is compact, its intent should be immediately obvious. Variables should describe their purpose, and comments should explain non-obvious logic or design decisions, not just restate what the code does. This ensures that anyone (including the original author in the future) can quickly grasp the editor's functionality.
Error handling and resource management are also crucial. Every malloc should ideally have a corresponding free. File operations should check for NULL pointers or EOF to handle failures gracefully. A minimalist editor might not have elaborate error messages, but it should at least prevent crashes and ensure resources are released. This attention to detail contributes significantly to the editor's robustness and reliability, even in its simplified form.
Conclusion
Building the smallest, console-based, portable code editor is an exercise in disciplined design and efficient implementation. By focusing on the absolute essentials – raw terminal interaction, robust text buffer management, efficient screen rendering, precise input processing, and reliable file operations – such an editor can provide a powerful, distraction-free coding environment. The journey involves careful consideration of cross-platform compatibility and a steadfast commitment to clean code principles, ensuring that even the leanest tool remains a joy to use and maintain. This type of editor proves that powerful functionality does not always require a large, complex application, but rather a thoughtful and focused approach to software development.
Addendum: Full Running Example Code
This C program implements a basic, ultra-portable, console-based text editor. It demonstrates the core concepts discussed in the article, including raw terminal input, ANSI escape code-based screen rendering, a dynamic line-based text buffer, and fundamental file I/O. It includes conditional compilation for basic cross-platform support between Unix-like systems and Windows.
To compile on Unix-like systems (Linux, macOS):
gcc -o microedit microedit.c
To compile on Windows (using MinGW/GCC):
gcc -o microedit.exe microedit.c
To run: ./microedit [filename]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// Platform-specific headers for terminal I/O
#ifdef _WIN32
#include <windows.h>
#include <conio.h> // For _getch()
#else
#include <termios.h>
#include <unistd.h>
#include <sys/ioctl.h> // For ioctl() to get terminal size
#endif
// --- Constants and Macros ---
#define KILO_VERSION "0.0.1"
#define KILO_TAB_STOP 8 // Number of spaces for a tab
// Define key codes for common special keys
// These are often derived from ANSI escape sequences or platform-specific values
#define ARROW_LEFT 1000
#define ARROW_RIGHT 1001
#define ARROW_UP 1002
#define ARROW_DOWN 1003
#define PAGE_UP 1004
#define PAGE_DOWN 1005
#define HOME_KEY 1006
#define END_KEY 1007
#define DELETE_KEY 1008
#define BACKSPACE 127
#define ENTER '\r'
#define ESC '\x1b'
// Control key macro (e.g., CTRL_KEY('q') for Ctrl+Q)
#define CTRL_KEY(k) ((k) & 0x1f)
// --- Data Structures ---
// Structure to represent a single line of text in the editor's buffer
typedef struct {
char *chars; // Pointer to the actual character data of the line
int length; // Current number of characters in the line (excluding null terminator)
int capacity; // Allocated memory capacity for the line buffer
} Line;
// Structure to hold the global editor state
typedef struct {
int cursorX; // Editor's internal cursor column position (0-based)
int cursorY; // Editor's internal cursor row position (0-based)
int screenRows; // Number of rows available in the terminal window
int screenCols; // Number of columns available in the terminal window
int rowOffset; // Vertical scroll offset (topmost line displayed on screen)
int colOffset; // Horizontal scroll offset (leftmost column displayed on screen)
Line *lines; // Array of Line structures, representing the document
int numLines; // Total number of lines in the document
char *filename; // Name of the file being edited
int dirty; // Flag indicating if the file has unsaved changes (1 = dirty, 0 = clean)
#ifdef _WIN32
HANDLE hStdin; // Handle to standard input for Windows console API
DWORD dwOriginalInMode; // Original console input mode for Windows
HANDLE hStdout; // Handle to standard output for Windows console API
DWORD dwOriginalOutMode; // Original console output mode for Windows
#else
struct termios original_termios; // Original terminal attributes for Unix-like systems
#endif
} EditorState;
// Global instance of the editor state
EditorState E;
// --- Terminal I/O Functions (Platform-specific) ---
#ifdef _WIN32
// Windows-specific function to enable raw mode
void enableRawMode() {
E.hStdin = GetStdHandle(STD_INPUT_HANDLE);
E.hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleMode(E.hStdin, &E.dwOriginalInMode);
GetConsoleMode(E.hStdout, &E.dwOriginalOutMode);
// Disable processed input, echo input, line input, window input, mouse input
SetConsoleMode(E.hStdin, ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_WINDOW_INPUT);
// Enable virtual terminal processing for ANSI escape codes
SetConsoleMode(E.hStdout, ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN);
// Get screen dimensions
CONSOLE_SCREEN_BUFFER_INFO csbi;
GetConsoleScreenBufferInfo(E.hStdout, &csbi);
E.screenCols = csbi.srWindow.Right - csbi.srWindow.Left + 1;
E.screenRows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
}
// Windows-specific function to disable raw mode
void disableRawMode() {
SetConsoleMode(E.hStdin, E.dwOriginalInMode);
SetConsoleMode(E.hStdout, E.dwOriginalOutMode);
}
// Windows-specific function to read a key
int readKey() {
int c = _getch(); // Reads a character without echoing
if (c == 0 || c == 224) { // Special key (e.g., arrow keys)
c = _getch(); // Read the second byte of the scan code
switch (c) {
case 75: return ARROW_LEFT;
case 77: return ARROW_RIGHT;
case 72: return ARROW_UP;
case 80: return ARROW_DOWN;
case 73: return PAGE_UP;
case 81: return PAGE_DOWN;
case 71: return HOME_KEY;
case 79: return END_KEY;
case 83: return DELETE_KEY;
default: return c + 10000; // Return a unique value for other special keys
}
}
return c;
}
// Windows-specific function to get terminal size
int getWindowSize(int *rows, int *cols) {
CONSOLE_SCREEN_BUFFER_INFO csbi;
if (GetConsoleScreenBufferInfo(E.hStdout, &csbi)) {
*cols = csbi.srWindow.Right - csbi.srWindow.Left + 1;
*rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
return 0;
} else {
return -1;
}
}
#else // Unix-like systems
// Unix-like specific function to enable raw mode
void enableRawMode() {
// Get the current terminal attributes
tcgetattr(STDIN_FILENO, &E.original_termios);
// Create a copy to modify
struct termios raw = E.original_termios;
// Disable canonical mode (line-by-line input), echo, signal generation, extended input processing
raw.c_lflag &= ~(ECHO | ICANON | ISIG | IEXTEN);
// Disable output post-processing (e.g., \n to \r\n conversion)
raw.c_oflag &= ~(OPOST);
// Set character size to 8 bits
raw.c_cflag |= (CS8);
// Disable software flow control, disable Ctrl-V, Ctrl-O
raw.c_iflag &= ~(BRKINT | ICRNL | INPCK | ISTRIP | IXON);
// Set minimum number of characters for non-canonical read
raw.c_cc[VMIN] = 0;
// Set timeout for read in tenths of a second
raw.c_cc[VTIME] = 1;
// Apply the new attributes immediately
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
}
// Unix-like specific function to disable raw mode
void disableRawMode() {
// Restore the terminal attributes to their original state
tcsetattr(STDIN_FILENO, TCSAFLUSH, &E.original_termios);
}
// Unix-like specific function to read a key
int readKey() {
int nread;
char c;
while ((nread = read(STDIN_FILENO, &c, 1)) == 0);
if (nread == -1) {
// Handle read error, a robust editor would log this
return -1;
}
// Handle ANSI escape sequences for special keys
if (c == ESC) {
char seq[3];
if (read(STDIN_FILENO, &seq[0], 1) == 0) return ESC;
if (read(STDIN_FILENO, &seq[1], 1) == 0) return ESC;
if (seq[0] == '[') {
if (seq[1] >= '0' && seq[1] <= '9') {
if (read(STDIN_FILENO, &seq[2], 1) == 0) return ESC;
if (seq[2] == '~') {
switch (seq[1]) {
case '1': return HOME_KEY;
case '3': return DELETE_KEY;
case '4': return END_KEY;
case '5': return PAGE_UP;
case '6': return PAGE_DOWN;
case '7': return HOME_KEY;
case '8': return END_KEY;
}
}
} else {
switch (seq[1]) {
case 'A': return ARROW_UP;
case 'B': return ARROW_DOWN;
case 'C': return ARROW_RIGHT;
case 'D': return ARROW_LEFT;
case 'H': return HOME_KEY;
case 'F': return END_KEY;
}
}
} else if (seq[0] == 'O') {
switch (seq[1]) {
case 'H': return HOME_KEY;
case 'F': return END_KEY;
}
}
return ESC; // Unrecognized escape sequence
} else {
return c;
}
}
// Unix-like specific function to get terminal size
int getWindowSize(int *rows, int *cols) {
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0) {
// Fallback if ioctl fails: move cursor to bottom right and read position
if (write(STDOUT_FILENO, "\x1b[999C\x1b[999B", 12) != 12) return -1;
char buf[32];
unsigned int i = 0;
if (read(STDIN_FILENO, buf, sizeof(buf) - 1) == 0) return -1;
buf[i] = '\0';
if (buf[0] != ESC || buf[1] != '[') return -1;
if (sscanf(&buf[2], "%d;%d", rows, cols) != 2) return -1;
return 0;
} else {
*cols = ws.ws_col;
*rows = ws.ws_row;
return 0;
}
}
#endif
// --- Utility Functions ---
// Function to clear the entire terminal screen
void clearScreen() {
printf("\x1b[2J");
}
// Function to move the cursor to a specific row and column (1-based)
void moveCursor(int row, int col) {
printf("\x1b[%d;%dH", row, col);
}
// Function to hide the cursor
void hideCursor() {
printf("\x1b[?25l");
}
// Function to show the cursor
void showCursor() {
printf("\x1b[?25h");
}
// Function to set text color (e.g., "\x1b[31m" for red)
void setForegroundColor(const char *colorCode) {
printf("%s", colorCode);
}
// Function to reset text color to default
void resetColor() {
printf("\x1b[39m");
}
// --- Line Management Functions ---
// Define a reasonable initial capacity for a line
#define LINE_INITIAL_CAPACITY 16
// Function to initialize a new line structure
void initLine(Line *line) {
line->chars = NULL;
line->length = 0;
line->capacity = 0;
}
// Function to free memory associated with a line
void freeLine(Line *line) {
if (line->chars != NULL) {
free(line->chars);
line->chars = NULL;
}
line->length = 0;
line->capacity = 0;
}
// Function to reallocate memory for a line if needed
// Ensures enough capacity for 'required_length' characters + null terminator
int ensureLineCapacity(Line *line, int required_length) {
if (required_length + 1 > line->capacity) { // +1 for null terminator
int newCapacity = line->capacity == 0 ? LINE_INITIAL_CAPACITY : line->capacity * 2;
while (newCapacity < required_length + 1) {
newCapacity *= 2;
}
char *newChars = (char *)realloc(line->chars, newCapacity);
if (newChars == NULL) {
return 0; // Memory allocation failed
}
line->chars = newChars;
line->capacity = newCapacity;
}
return 1; // Success
}
// Function to insert a character into a line at a specific position
// Returns 1 on success, 0 on memory allocation failure
int insertCharIntoLine(Line *line, int at, int c) {
if (at < 0 || at > line->length) at = line->length; // Clamp 'at' to valid range
if (!ensureLineCapacity(line, line->length + 1)) {
return 0; // Memory allocation failed
}
// Shift characters to the right to make space for the new character
memmove(&line->chars[at + 1], &line->chars[at], line->length - at);
// Insert the new character and update length
line->chars[at] = (char)c;
line->length++;
line->chars[line->length] = '\0'; // Null-terminate the string
E.dirty = 1; // Mark editor state as dirty (unsaved changes)
return 1; // Success
}
// Function to delete a character from a line at a specific position
// Returns 1 on success, 0 if 'at' is out of bounds
int deleteCharFromLine(Line *line, int at) {
if (at < 0 || at >= line->length) return 0; // Out of bounds
// Shift characters to the left to overwrite the deleted character
memmove(&line->chars[at], &line->chars[at + 1], line->length - at);
// Update length and null-terminate
line->length--;
line->chars[line->length] = '\0';
E.dirty = 1; // Mark editor state as dirty
return 1; // Success
}
// Function to insert a new line into the document at a specific row
// Returns 1 on success, 0 on memory allocation failure
int insertLine(int at, const char *s, int len) {
if (at < 0 || at > E.numLines) return 0; // Clamp 'at'
// Ensure enough space in the lines array
// Dynamically reallocate E.lines if needed
if (E.numLines >= E.screenRows) { // Simple heuristic: double capacity if full
int newCapacity = E.numLines == 0 ? 10 : E.numLines * 2;
Line *newLines = (Line *)realloc(E.lines, sizeof(Line) * newCapacity);
if (newLines == NULL) {
return 0; // Memory allocation failed
}
E.lines = newLines;
}
// Shift existing lines down to make space for the new line
memmove(&E.lines[at + 1], &E.lines[at], sizeof(Line) * (E.numLines - at));
// Initialize the new line and copy content
initLine(&E.lines[at]);
if (!ensureLineCapacity(&E.lines[at], len)) {
return 0; // Memory allocation failed
}
memcpy(E.lines[at].chars, s, len);
E.lines[at].length = len;
E.lines[at].chars[len] = '\0'; // Null-terminate
E.numLines++;
E.dirty = 1; // Mark editor state as dirty
return 1; // Success
}
// Function to delete a line from the document at a specific row
// Returns 1 on success, 0 on failure
int deleteLine(int at) {
if (at < 0 || at >= E.numLines) return 0; // Out of bounds
freeLine(&E.lines[at]); // Free memory for the line being deleted
// Shift subsequent lines up to fill the gap
memmove(&E.lines[at], &E.lines[at + 1], sizeof(Line) * (E.numLines - at - 1));
E.numLines--;
E.dirty = 1; // Mark editor state as dirty
return 1; // Success
}
// --- Editor Operations ---
// Inserts a character at the current cursor position
void editorInsertChar(int c) {
if (E.cursorY == E.numLines) {
// If cursor is on a new line below file content, add a new empty line
insertLine(E.numLines, "", 0);
}
if (insertCharIntoLine(&E.lines[E.cursorY], E.cursorX, c)) {
E.cursorX++; // Move cursor after inserted character
}
}
// Deletes a character at the current cursor position (or before it if Backspace)
void editorDeleteChar() {
if (E.cursorY == E.numLines) return; // Nothing to delete below file
if (E.cursorX == 0 && E.cursorY == 0) return; // Cannot delete before first char
if (E.cursorX > 0) {
// Delete character before cursor on the current line
deleteCharFromLine(&E.lines[E.cursorY], E.cursorX - 1);
E.cursorX--;
} else {
// If cursor is at the beginning of a line, join it with the previous line
E.cursorX = E.lines[E.cursorY - 1].length; // Move cursor to end of previous line
Line *currentLine = &E.lines[E.cursorY];
Line *prevLine = &E.lines[E.cursorY - 1];
// Ensure previous line has capacity for current line's content
if (!ensureLineCapacity(prevLine, prevLine->length + currentLine->length)) {
return; // Handle memory allocation failure
}
// Append current line's content to previous line
memcpy(&prevLine->chars[prevLine->length], currentLine->chars, currentLine->length);
prevLine->length += currentLine->length;
prevLine->chars[prevLine->length] = '\0'; // Null-terminate
deleteLine(E.cursorY); // Delete the now-empty current line
E.cursorY--; // Move cursor up to the previous line
}
}
// Inserts a newline character, splitting the current line or adding a new one
void editorInsertNewline() {
if (E.cursorX == 0) {
// If cursor is at beginning of line, insert new empty line above
insertLine(E.cursorY, "", 0);
} else {
// Split the current line at cursorX
Line *currentLine = &E.lines[E.cursorY];
insertLine(E.cursorY + 1, ¤tLine->chars[E.cursorX], currentLine->length - E.cursorX);
currentLine->length = E.cursorX;
currentLine->chars[currentLine->length] = '\0'; // Null-terminate
}
E.cursorY++;
E.cursorX = 0;
}
// --- File I/O Functions ---
// Function to load a file into the editor's buffer
// Returns 1 on success, 0 on failure
int editorOpenFile(const char *filename) {
free(E.filename);
E.filename = strdup(filename); // Store filename
FILE *fp = fopen(filename, "r");
if (fp == NULL) {
// If file doesn't exist, start with an empty buffer
E.numLines = 0;
E.lines = NULL;
E.dirty = 0;
return 1; // Not an error, just a new file
}
char *lineBuffer = NULL;
size_t bufferSize = 0;
ssize_t readLength;
// Use a portable way to read lines (fgets with dynamic buffer)
while (1) {
int c;
int currentLineLen = 0;
int currentLineCap = LINE_INITIAL_CAPACITY;
char *currentLineChars = (char *)malloc(currentLineCap);
if (currentLineChars == NULL) {
// Memory allocation failed, clean up and return
if (lineBuffer) free(lineBuffer);
fclose(fp);
for (int i = 0; i < E.numLines; i++) freeLine(&E.lines[i]);
free(E.lines);
E.lines = NULL;
E.numLines = 0;
return 0;
}
while ((c = fgetc(fp)) != EOF && c != '\n') {
if (currentLineLen >= currentLineCap - 1) { // -1 for null terminator
currentLineCap *= 2;
char *newChars = (char *)realloc(currentLineChars, currentLineCap);
if (newChars == NULL) {
free(currentLineChars);
if (lineBuffer) free(lineBuffer);
fclose(fp);
for (int i = 0; i < E.numLines; i++) freeLine(&E.lines[i]);
free(E.lines);
E.lines = NULL;
E.numLines = 0;
return 0;
}
currentLineChars = newChars;
}
currentLineChars[currentLineLen++] = (char)c;
}
currentLineChars[currentLineLen] = '\0'; // Null-terminate the line
// Insert the read line into the editor's buffer
if (!insertLine(E.numLines, currentLineChars, currentLineLen)) {
free(currentLineChars);
if (lineBuffer) free(lineBuffer);
fclose(fp);
for (int i = 0; i < E.numLines; i++) freeLine(&E.lines[i]);
free(E.lines);
E.lines = NULL;
E.numLines = 0;
return 0;
}
free(currentLineChars); // Free temporary buffer for the line
if (c == EOF) break; // End of file reached
}
if (lineBuffer) free(lineBuffer); // Free getline buffer if used
fclose(fp);
E.dirty = 0; // File is clean after loading
return 1; // Success
}
// Function to save the editor's buffer to a file
// Returns 1 on success, 0 on failure
int editorSaveFile() {
if (E.filename == NULL) {
// In a real editor, prompt for filename. For this minimalist version,
// we assume filename is provided at startup or editor won't save.
return 0;
}
FILE *fp = fopen(E.filename, "w"); // Open in write mode, truncates if exists
if (fp == NULL) {
return 0; // Error opening file for writing
}
for (int i = 0; i < E.numLines; i++) {
if (E.lines[i].chars != NULL) {
if (fputs(E.lines[i].chars, fp) == EOF) {
fclose(fp);
return 0; // Error writing to file
}
}
if (fputc('\n', fp) == EOF) { // Write newline character after each line
fclose(fp);
return 0; // Error writing newline
}
}
fclose(fp);
E.dirty = 0; // File is now clean
return 1; // Success
}
// --- Editor Initialization and Cleanup ---
// Initializes the editor state
void initEditor() {
E.cursorX = 0;
E.cursorY = 0;
E.rowOffset = 0;
E.colOffset = 0;
E.numLines = 0;
E.lines = NULL;
E.filename = NULL;
E.dirty = 0;
// Get initial window size
if (getWindowSize(&E.screenRows, &E.screenCols) == -1) {
// Fallback or error handling if size cannot be determined
E.screenRows = 24;
E.screenCols = 80;
}
// Reserve one row for status bar
E.screenRows--;
}
// Frees all allocated memory and cleans up editor state
void freeEditor() {
for (int i = 0; i < E.numLines; i++) {
freeLine(&E.lines[i]);
}
if (E.lines != NULL) {
free(E.lines);
E.lines = NULL;
}
if (E.filename != NULL) {
free(E.filename);
E.filename = NULL;
}
}
// --- Screen Refresh and Drawing ---
// Scrolls the view to ensure the cursor is visible
void editorScroll() {
// Vertical scrolling
if (E.cursorY < E.rowOffset) {
E.rowOffset = E.cursorY;
}
if (E.cursorY >= E.rowOffset + E.screenRows) {
E.rowOffset = E.cursorY - E.screenRows + 1;
}
// Horizontal scrolling (simplified for this example, usually more complex for wrapped lines)
if (E.cursorX < E.colOffset) {
E.colOffset = E.cursorX;
}
if (E.cursorX >= E.colOffset + E.screenCols) {
E.colOffset = E.cursorX - E.screenCols + 1;
}
}
// Draws the editor's content to the screen
void editorDrawScreen() {
editorScroll(); // Adjust scroll offsets before drawing
hideCursor(); // Hide cursor during redraw to prevent flickering
clearScreen(); // Clear the entire screen
// Iterate through visible screen rows
for (int y = 0; y < E.screenRows; y++) {
int fileRow = y + E.rowOffset; // Calculate corresponding file row in buffer
if (fileRow < E.numLines) {
// Draw the actual line from the buffer
Line *line = &E.lines[fileRow];
int len_to_print = line->length - E.colOffset;
if (len_to_print < 0) len_to_print = 0;
if (len_to_print > E.screenCols) len_to_print = E.screenCols;
moveCursor(y + 1, 1); // Move cursor to start of current screen row
printf("%.*s", len_to_print, &line->chars[E.colOffset]);
} else if (fileRow == E.numLines && E.numLines == 0) {
// Display welcome message if file is empty
if (y == E.screenRows / 3) {
char welcome[80];
int welcomelen = snprintf(welcome, sizeof(welcome),
"MicroEdit -- Version %s", KILO_VERSION);
if (welcomelen > E.screenCols) welcomelen = E.screenCols;
int padding = (E.screenCols - welcomelen) / 2;
if (padding) {
printf("~");
while (padding--) printf(" ");
}
printf("%.*s", welcomelen, welcome);
} else {
printf("~"); // Draw a tilde for empty lines below welcome
}
} else {
printf("~"); // Draw a tilde for empty lines below file content
}
printf("\x1b[K"); // Clear from cursor to end of line
}
// Draw status bar at the bottom
moveCursor(E.screenRows + 1, 1);
setForegroundColor("\x1b[47m\x1b[30m"); // White background, black text
char status[80];
char rstatus[80];
int len = snprintf(status, sizeof(status), "%.20s - %d lines %s",
E.filename ? E.filename : "[No Name]", E.numLines,
E.dirty ? "(modified)" : "");
int rlen = snprintf(rstatus, sizeof(rstatus), "%d/%d", E.cursorY + 1, E.numLines);
if (len > E.screenCols) len = E.screenCols;
printf("%.*s", len, status);
while (len < E.screenCols) {
if (E.screenCols - len == rlen) {
printf("%s", rstatus);
break;
} else {
printf(" ");
len++;
}
}
resetColor(); // Reset colors after status bar
// Position the cursor at the editor's internal cursor position
moveCursor(E.cursorY - E.rowOffset + 1, E.cursorX - E.colOffset + 1);
showCursor(); // Show cursor after redraw
fflush(stdout); // Ensure all output is written to terminal
}
// --- Input Processing ---
// Moves the editor's cursor based on key press
void editorMoveCursor(int key) {
Line *line = (E.cursorY >= 0 && E.cursorY < E.numLines) ? &E.lines[E.cursorY] : NULL;
switch (key) {
case ARROW_LEFT:
if (E.cursorX > 0) {
E.cursorX--;
} else if (E.cursorY > 0) {
// Move to end of previous line
E.cursorY--;
E.cursorX = E.lines[E.cursorY].length;
}
break;
case ARROW_RIGHT:
if (line && E.cursorX < line->length) {
E.cursorX++;
} else if (line && E.cursorX == line->length) {
// Move to beginning of next line
if (E.cursorY < E.numLines - 1) {
E.cursorY++;
E.cursorX = 0;
}
}
break;
case ARROW_UP:
if (E.cursorY > 0) {
E.cursorY--;
}
break;
case ARROW_DOWN:
if (E.cursorY < E.numLines) { // Allow moving one line past content (for new line)
E.cursorY++;
}
break;
case HOME_KEY:
E.cursorX = 0;
break;
case END_KEY:
if (line) {
E.cursorX = line->length;
}
break;
}
// Clamp cursorX to the end of the current line (prevents going past end when moving vertically)
line = (E.cursorY >= 0 && E.cursorY < E.numLines) ? &E.lines[E.cursorY] : NULL;
int lineLen = line ? line->length : 0;
if (E.cursorX > lineLen) {
E.cursorX = lineLen;
}
}
// Processes a key press and performs corresponding editor action
void editorProcessKeyPress() {
int c = readKey();
switch (c) {
case CTRL_KEY('q'): // Ctrl+Q to quit
if (E.dirty) {
// In a real editor, prompt to save changes.
// For this minimalist version, we just quit.
}
clearScreen();
moveCursor(1,1);
printf("Exiting MicroEdit.\n");
exit(0);
break;
case CTRL_KEY('s'): // Ctrl+S to save
if (editorSaveFile()) {
// printf message on status bar
} else {
// printf error message
}
break;
case ENTER:
editorInsertNewline();
break;
case BACKSPACE:
case DELETE_KEY:
if (c == DELETE_KEY) {
// For DELETE_KEY, if cursor is at end of line, move right then delete
if (E.cursorX == E.lines[E.cursorY].length && E.cursorY < E.numLines -1) {
editorMoveCursor(ARROW_RIGHT);
}
}
editorDeleteChar();
break;
case PAGE_UP:
case PAGE_DOWN:
{
int times = E.screenRows;
while (times--) {
editorMoveCursor(c == PAGE_UP ? ARROW_UP : ARROW_DOWN);
}
}
break;
case ARROW_UP:
case ARROW_DOWN:
case ARROW_LEFT:
case ARROW_RIGHT:
case HOME_KEY:
case END_KEY:
editorMoveCursor(c);
break;
case ESC: // Ignore other escape sequences for now
break;
default:
// If it's a printable character, insert it into the buffer
if (c >= 32 && c <= 126) { // ASCII printable characters
editorInsertChar(c);
}
break;
}
}
// --- Main Loop ---
int main(int argc, char *argv[]) {
initEditor();
enableRawMode();
// Load file if provided as argument
if (argc >= 2) {
if (!editorOpenFile(argv[1])) {
clearScreen();
moveCursor(1,1);
printf("Error: Could not open file '%s'. Starting new file.\n", argv[1]);
// Optionally, exit here if file opening is critical
}
}
// If no lines were loaded, add an initial empty line
if (E.numLines == 0) {
insertLine(0, "", 0);
E.dirty = 0; // New file is not dirty
}
// Main editor loop
while (1) {
editorDrawScreen();
editorProcessKeyPress();
}
// Cleanup (this part is technically unreachable due to exit() in editorProcessKeyPress)
freeEditor();
disableRawMode();
return 0;
}
No comments:
Post a Comment