Chapter 3: Raw Input and Output
Chapter 3: Raw Input and Output
Now that we have our basic editor structure in place, let’s improve how we handle terminal input and output. In this chapter, we’ll implement raw mode input handling, support for special keys, and efficient screen rendering.
Understanding Terminal Modes
Let’s start by understanding the difference between terminal modes:
- Canonical mode (default): Input is processed line-by-line, only sent after pressing Enter
- Raw mode: Input is processed character-by-character immediately, giving us full control
For a text editor, raw mode is essential because we need to:
- Process each keystroke immediately
- Handle special keys (arrows, Home, End, etc.)
- Control exactly what gets displayed on the screen
Setting Up Key Definitions
First, let’s define constants for special keys. Create or modify the file include/kilo++/EditorUtils.hpp:
// include/kilo++/EditorUtils.hpp
#pragma once
enum class EditorKey
{
BACKSPACE = 127,
ARROW_LEFT = 1000,
ARROW_RIGHT,
ARROW_UP,
ARROW_DOWN,
DEL_KEY,
HOME_KEY,
END_KEY,
PAGE_UP,
PAGE_DOWN
};
namespace terminal_manager
{
void die(const char *s);
void disableRawMode();
void enableRawMode();
int getWindowSize(int *rows, int *cols);
int readKey();
}
We use an enum class (a C++11 feature) to define our special keys:
BACKSPACEis assigned its ASCII value (127)- The arrow keys and other special keys start at 1000 to avoid conflicts with ASCII characters
- Each subsequent key gets automatically assigned the next integer value
Implementing Terminal Management
Now let’s implement these functions in src/EditorUtils.cpp:
// src/EditorUtils.cpp
#include "kilo++/EditorUtils.hpp"
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
namespace terminal_manager
{
// Store the original terminal settings
termios orig_termios;
Error Handling Function
First, let’s implement a utility function to handle errors:
void die(const char *s)
{
// Clear screen before displaying error
write(STDOUT_FILENO, "\x1b[2J", 4);
write(STDOUT_FILENO, "\x1b[H", 3);
perror(s);
exit(1);
}
This function:
- Clears the screen using ANSI escape sequences
- Positions the cursor at the top-left corner
- Calls
perror()to display the error message with system context - Exits the program with an error code
Raw Mode Management
Next, let’s implement the functions to enable and disable raw mode:
void disableRawMode()
{
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &orig_termios) == -1)
die("tcsetattr");
}
void enableRawMode()
{
// Get current terminal attributes
if (tcgetattr(STDIN_FILENO, &orig_termios) == -1)
die("tcgetattr");
// Register disableRawMode to be called at exit
atexit(disableRawMode);
// Modify terminal attributes for raw mode
struct termios raw = orig_termios;
// Input flags: disable break signal, CR to NL conversion,
// stripping of 8th bit, and software flow control
raw.c_iflag &= ~(BRKINT | ICRNL | ISTRIP | IXON);
// Output flags: disable post-processing
raw.c_oflag &= ~(OPOST);
// Control flags: set 8-bit characters
raw.c_cflag |= (CS8);
// Local flags: disable echo, canonical mode, special keys, and signals
raw.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
// Control characters: set read timeout
raw.c_cc[VMIN] = 0; // Return immediately, even if no input is available
raw.c_cc[VTIME] = 1; // Wait up to 1/10 second for input
// Apply the modified attributes
if (tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw) == -1)
die("tcsetattr");
}
Let’s break down what we’re doing in enableRawMode():
- First, we save the original terminal attributes so we can restore them later
- We register our
disableRawMode()function to run when the program exits - We create a copy of the original attributes to modify
- We turn off several terminal features by using the bitwise NOT (
~) and bitwise AND (&=) operators:BRKINT: Disable break signalsICRNL: Disable carriage return to newline translationISTRIP: Disable stripping of the 8th bitIXON: Disable software flow control (Ctrl+S, Ctrl+Q)OPOST: Disable output processingECHO: Disable character echoICANON: Enable raw mode (process input byte-by-byte)IEXTEN: Disable extended input processingISIG: Disable signal processing (Ctrl+C, Ctrl+Z)
- We set
CS8(control flag) to use 8 bits per byte - We set
VMINto 0 andVTIMEto 1 for a 100ms timeout on input - Finally, we apply these settings using
tcsetattr()
Getting Terminal Size
Next, let’s implement a function to get the terminal size:
int getWindowSize(int *rows, int *cols)
{
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1 || ws.ws_col == 0)
return -1;
*cols = ws.ws_col;
*rows = ws.ws_row;
return 0;
}
This function:
- Creates a
winsizestructure to hold the terminal dimensions - Uses
ioctl()withTIOCGWINSZcommand to query the terminal size - If successful, it sets the values through the pointers provided
- Returns 0 on success or -1 on failure
Reading Input with Support for Special Keys
Finally, let’s implement the key reading function, which is the most complex part:
int readKey()
{
int nread;
char c;
// Keep trying until we read a character or get an error
while ((nread = read(STDIN_FILENO, &c, 1)) != 1)
{
if (nread == -1 && errno != EAGAIN)
die("read");
}
// If we read an escape sequence
if (c == '\x1b')
{
char seq[3];
// Try to read the next two bytes with a short timeout
if (read(STDIN_FILENO, &seq[0], 1) != 1) return '\x1b';
if (read(STDIN_FILENO, &seq[1], 1) != 1) return '\x1b';
// Check if it's an escape sequence we recognize
if (seq[0] == '[')
{
if (seq[1] >= '0' && seq[1] <= '9')
{
// Extended escape sequence, read one more byte
if (read(STDIN_FILENO, &seq[2], 1) != 1) return '\x1b';
if (seq[2] == '~')
{
// Map to our special keys
switch (seq[1])
{
case '1': return static_cast<int>(EditorKey::HOME_KEY);
case '3': return static_cast<int>(EditorKey::DEL_KEY);
case '4': return static_cast<int>(EditorKey::END_KEY);
case '5': return static_cast<int>(EditorKey::PAGE_UP);
case '6': return static_cast<int>(EditorKey::PAGE_DOWN);
case '7': return static_cast<int>(EditorKey::HOME_KEY);
case '8': return static_cast<int>(EditorKey::END_KEY);
}
}
}
else
{
// Simple escape sequence for arrow keys and others
switch (seq[1])
{
case 'A': return static_cast<int>(EditorKey::ARROW_UP);
case 'B': return static_cast<int>(EditorKey::ARROW_DOWN);
case 'C': return static_cast<int>(EditorKey::ARROW_RIGHT);
case 'D': return static_cast<int>(EditorKey::ARROW_LEFT);
case 'H': return static_cast<int>(EditorKey::HOME_KEY);
case 'F': return static_cast<int>(EditorKey::END_KEY);
}
}
}
else if (seq[0] == 'O')
{
// Another format for Home and End keys
switch (seq[1])
{
case 'H': return static_cast<int>(EditorKey::HOME_KEY);
case 'F': return static_cast<int>(EditorKey::END_KEY);
}
}
// Unrecognized escape sequence
return '\x1b';
}
else
{
// Regular character
return c;
}
}
} // namespace terminal_manager
Let’s examine this function:
- It reads a single byte from standard input
- If the byte is an escape character (
\x1b), it tries to read the next two bytes with a short timeout - It looks for recognized escape sequences that match arrow keys, Home, End, Page Up, Page Down, and Delete
- If it recognizes a sequence, it returns the corresponding
EditorKeyvalue - If it doesn’t recognize the sequence, it returns the escape character
- For regular characters, it returns the character itself
Updating the Editor Class
Now we need to update our Editor class to use these new functions. Let’s modify include/kilo++/Editor.hpp:
// include/kilo++/Editor.hpp
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <time.h>
class Editor {
public:
Editor();
void run(int argc, char *argv[]);
void processKeypress();
void refreshScreen();
private:
// Input methods
void moveCursor(int key);
// Output methods
void drawRows(std::string &s);
void scroll();
void setStatusMessage(const char *fmt, ...);
// Member variables
std::vector<std::string> m_rows;
int m_cx = 0, m_cy = 0; // Cursor position
int m_rx = 0; // Render position (for tab rendering)
int m_rowoff = 0, m_coloff = 0; // Scroll offsets
int m_screenrows, m_screencols; // Terminal dimensions
std::string m_statusmsg = ""; // Status message
time_t m_statusmsg_time = 0; // When the status message was set
};
Implementing Enhanced Editor Functionality
Now let’s update the src/Editor.cpp file:
// src/Editor.cpp
#include "kilo++/Editor.hpp"
#include "kilo++/EditorUtils.hpp"
#include <cstdarg>
#include <cstring>
#include <unistd.h>
#define CTRL_KEY(k) ((k) & 0x1f)
First, let’s define the CTRL_KEY macro, which will help us work with control keys. This macro masks out all but the lower 5 bits of a key, which is what happens when you press Ctrl plus a key.
Constructor
Editor::Editor()
{
// Initialize terminal in raw mode
terminal_manager::enableRawMode();
// Get terminal size
if (terminal_manager::getWindowSize(&m_screenrows, &m_screencols) == -1)
terminal_manager::die("getWindowSize");
// Reserve space for status bar and message line
m_screenrows -= 2;
}
The constructor:
- Enables raw mode using our terminal manager
- Gets the terminal dimensions
- Reserves two rows for the status bar and message line
Main Run Method
void Editor::run(int argc, char *argv[])
{
// Display welcome message
setStatusMessage("HELP: Ctrl-Q = quit");
// Main program loop
while (true) {
refreshScreen();
processKeypress();
}
}
The run() method:
- Sets an initial status message
- Enters the main loop, refreshing the screen and processing keypresses
Processing Input
void Editor::processKeypress()
{
// Read a key
int c = terminal_manager::readKey();
// Handle the key
switch (c) {
case CTRL_KEY('q'):
// Clear screen and exit
write(STDOUT_FILENO, "\x1b[2J", 4);
write(STDOUT_FILENO, "\x1b[H", 3);
exit(0);
break;
case static_cast<int>(EditorKey::ARROW_UP):
case static_cast<int>(EditorKey::ARROW_DOWN):
case static_cast<int>(EditorKey::ARROW_LEFT):
case static_cast<int>(EditorKey::ARROW_RIGHT):
// Move cursor
moveCursor(c);
break;
case static_cast<int>(EditorKey::PAGE_UP):
case static_cast<int>(EditorKey::PAGE_DOWN):
{
// Move cursor by screenful
int times = m_screenrows;
while (times--)
moveCursor(c == static_cast<int>(EditorKey::PAGE_UP) ?
static_cast<int>(EditorKey::ARROW_UP) :
static_cast<int>(EditorKey::ARROW_DOWN));
}
break;
case static_cast<int>(EditorKey::HOME_KEY):
// Move to beginning of line
m_cx = 0;
break;
case static_cast<int>(EditorKey::END_KEY):
// Move to end of line
if (m_cy < m_rows.size())
m_cx = m_rows[m_cy].size();
break;
}
}
This method:
- Reads a key using our
readKey()function - Handles different key presses:
- Ctrl+Q quits the program
- Arrow keys move the cursor
- Page Up/Down moves by a screenful
- Home/End move to the beginning/end of the line
Moving the Cursor
void Editor::moveCursor(int key)
{
// Handle cursor movement based on key
switch (key) {
case static_cast<int>(EditorKey::ARROW_LEFT):
if (m_cx > 0) {
m_cx--;
} else if (m_cy > 0) {
// Move to end of previous line
m_cy--;
m_cx = m_rows.empty() || m_cy >= m_rows.size() ? 0 : m_rows[m_cy].size();
}
break;
case static_cast<int>(EditorKey::ARROW_RIGHT):
if (!m_rows.empty() && m_cy < m_rows.size()) {
if (m_cx < m_rows[m_cy].size()) {
m_cx++;
} else if (m_cx == m_rows[m_cy].size() && m_cy < m_rows.size() - 1) {
// Move to beginning of next line
m_cy++;
m_cx = 0;
}
}
break;
case static_cast<int>(EditorKey::ARROW_UP):
if (m_cy > 0) m_cy--;
break;
case static_cast<int>(EditorKey::ARROW_DOWN):
if (!m_rows.empty() && m_cy < m_rows.size() - 1) m_cy++;
break;
}
// Ensure cursor position is valid
if (!m_rows.empty() && m_cy < m_rows.size()) {
if (m_cx > m_rows[m_cy].size())
m_cx = m_rows[m_cy].size();
}
}
This method:
- Handles the four arrow keys
- For left/right, allows moving between lines
- For up/down, ensures we don’t go past the first or last line
- Ensures the cursor position is valid for the current line
Scrolling Logic
void Editor::scroll()
{
// Update render position
m_rx = m_cx;
// Vertical scrolling
if (m_cy < m_rowoff) {
// Scrolling up
m_rowoff = m_cy;
}
if (m_cy >= m_rowoff + m_screenrows) {
// Scrolling down
m_rowoff = m_cy - m_screenrows + 1;
}
// Horizontal scrolling
if (m_rx < m_coloff) {
// Scrolling left
m_coloff = m_rx;
}
if (m_rx >= m_coloff + m_screencols) {
// Scrolling right
m_coloff = m_rx - m_screencols + 1;
}
}
This method:
- Updates the render position (
m_rx) - Handles vertical scrolling when the cursor moves outside the visible area
- Handles horizontal scrolling when the cursor moves outside the visible area
Drawing Rows
void Editor::drawRows(std::string &s)
{
// Draw each row
for (int y = 0; y < m_screenrows; y++) {
// Calculate file row
int filerow = y + m_rowoff;
if (filerow >= static_cast<int>(m_rows.size())) {
// Display welcome message if no file is open
if (m_rows.empty() && y == m_screenrows / 3) {
// Create welcome message
std::string welcome = "Kilo++ editor -- version 0.0.1";
if (welcome.size() > static_cast<size_t>(m_screencols))
welcome.resize(m_screencols);
// Center the welcome message
int padding = (m_screencols - welcome.size()) / 2;
if (padding) {
s += "~";
padding--;
}
while (padding--) s += " ";
s += welcome;
} else {
// Empty line
s += "~";
}
} else if (filerow < static_cast<int>(m_rows.size())) {
// Display file content
int len = m_rows[filerow].size() - m_coloff;
if (len < 0) len = 0;
if (len > m_screencols) len = m_screencols;
if (len > 0)
s += m_rows[filerow].substr(m_coloff, len);
}
// Clear to end of line and add newline
s += "\x1b[K";
s += "\r\n";
}
}
This method:
- Loops through each row of the screen
- If we’re past the end of the file, displays a welcome message or a tilde
- If we’re within the file, displays the file content with proper scrolling
- Clears to the end of each line and adds a newline
Status Message
void Editor::setStatusMessage(const char *fmt, ...)
{
va_list ap;
va_start(ap, fmt);
char buf[80];
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
m_statusmsg = buf;
m_statusmsg_time = time(NULL);
}
This method:
- Takes a format string and variable arguments (like
printf) - Formats the message into a buffer
- Stores the message and the current time
Refreshing the Screen
void Editor::refreshScreen()
{
// Handle scrolling
scroll();
// Prepare output string
std::string buffer;
// Hide cursor during redraw
buffer += "\x1b[?25l";
// Position cursor at top-left
buffer += "\x1b[H";
// Draw rows
drawRows(buffer);
// Display status bar
buffer += "\x1b[7m"; // Inverted colors
std::string status = "New File";
if (status.size() > static_cast<size_t>(m_screencols))
status.resize(m_screencols);
std::string rstatus = std::to_string(m_cy + 1) + "," + std::to_string(m_cx + 1);
buffer += status;
buffer.append(m_screencols - status.size() - rstatus.size(), ' ');
buffer += rstatus;
buffer += "\x1b[m"; // Normal colors
buffer += "\r\n";
// Display status message
buffer += "\x1b[K";
if (!m_statusmsg.empty() && time(NULL) - m_statusmsg_time < 5) {
int msglen = m_statusmsg.size();
if (msglen > m_screencols) msglen = m_screencols;
buffer += m_statusmsg.substr(0, msglen);
}
// Position cursor
buffer += "\x1b[" + std::to_string((m_cy - m_rowoff) + 1) + ";"
+ std::to_string((m_rx - m_coloff) + 1) + "H";
// Show cursor
buffer += "\x1b[?25h";
// Write to terminal
write(STDOUT_FILENO, buffer.c_str(), buffer.size());
}
This method:
- Handles scrolling
- Builds a string containing all output
- Hides the cursor during redraw
- Draws rows, status bar, and message line
- Positions the cursor at the current position
- Shows the cursor
- Writes everything to the terminal in one go
Updating the Main Function
Finally, let’s update the main function in src/kilo.cpp:
// src/kilo.cpp
#include "kilo++/Editor.hpp"
int main(int argc, char **argv)
{
Editor editor;
editor.run(argc, argv);
return 0;
}
Building and Running
Now let’s build and run our improved editor:
cd build
cmake ..
make
./kilo++
When you run the program, you should now be able to:
- See a welcome message
- Move the cursor with arrow keys
- Jump to the beginning/end of lines with Home/End
- Scroll pages with Page Up/Down
- Quit with Ctrl+Q
Understanding ANSI Escape Sequences
Throughout this chapter, we’ve used several ANSI escape sequences:
\x1b[2J: Clears the entire screen\x1b[H: Positions the cursor at the top-left corner\x1b[K: Clears from cursor to the end of line\x1b[7m: Enables inverted colors\x1b[m: Resets text formatting\x1b[?25l: Hides the cursor\x1b[?25h: Shows the cursor\x1b[<row>;<col>H: Positions the cursor at the specified row and column
What We’ve Accomplished
In this chapter, we’ve:
- Implemented raw mode for immediate character-by-character input
- Added support for special keys like arrows, Home, End, Page Up/Down
- Created a scrolling mechanism for navigating through content
- Added a status bar and message line
- Improved the screen rendering with a buffer to prevent flicker
In the next chapter, we’ll add file I/O capabilities to open and display text files.
Further Enhancements
If you want to experiment further:
- Try adding support for more key combinations
- Implement a cursor that stays visible on screen (highlight the current character)
- Add line numbers to the display
- Experiment with different color schemes for the status bar
Remember, each of these improvements builds on the solid foundation we’ve established for handling raw terminal input and output.