Chapter 2: Basic Structure
Chapter 2: Basic Structure
Now that we have our development environment set up, let’s establish the foundation of our text editor. In this chapter, we’ll create the basic structure of our program using modern C++ practices.
The Editor Class
We’ll start by defining a simple Editor class that will be the core of our text editor. This class will manage the screen display and handle user input.
Create a new file include/kilo++/Editor.hpp:
// include/kilo++/Editor.hpp
#pragma once
#include <string>
#include <vector>
class Editor {
public:
Editor();
void run();
void processKeypress();
void refreshScreen();
private:
// Member variables
std::vector<std::string> m_rows;
int m_screenrows = 0;
int m_screencols = 0;
};
Let’s examine the components of this class:
- Public Methods:
Editor(): Constructor that initializes our editorrun(): Main loop that refreshes the screen and processes keypressesprocessKeypress(): Handles user inputrefreshScreen(): Updates the display
- Member Variables:
m_rows: Stores the text contentm_screenrowsandm_screencols: Hold the terminal dimensions
Implementing the Editor
Now, let’s implement the Editor class in src/Editor.cpp. We’ll go through each function one by one.
Create a new file src/Editor.cpp:
// src/Editor.cpp
#include "kilo++/Editor.hpp"
#include <iostream>
#include <unistd.h>
#include <sys/ioctl.h>
Constructor
First, let’s implement the constructor:
Editor::Editor() {
// Get terminal size
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1) {
m_screenrows = ws.ws_row;
m_screencols = ws.ws_col;
} else {
// Default size if we can't get the actual size
m_screenrows = 24;
m_screencols = 80;
}
}
The constructor’s purpose is to initialize the editor by determining the terminal size:
- It creates a
winsizestructure that will hold the terminal dimensions - It calls
ioctl()with theTIOCGWINSZcommand to query the terminal size - If successful, it stores the actual dimensions in our member variables
- If the call fails, it falls back to a standard 24x80 terminal size
Main Loop
Next, let’s implement the main loop:
void Editor::run() {
while (true) {
refreshScreen();
processKeypress();
}
}
The run() method creates the main control flow of our editor:
- It establishes an infinite loop
- In each iteration, it first refreshes the screen display
- Then it processes a single keypress from the user
- This continues until the program is explicitly terminated
Processing Input
Now, let’s implement the method to handle user input:
void Editor::processKeypress() {
// Read a character from standard input
char c;
std::cin.get(c);
// Quit if 'q' is pressed
if (c == 'q') {
// Clear the screen before exiting
std::cout << "\x1b[2J";
std::cout << "\x1b[H";
exit(0);
}
}
The processKeypress() method handles keyboard input:
- It reads a single character from standard input using
std::cin.get() - It checks if the character is ‘q’ (our quit command)
- If it is ‘q’, it:
- Clears the screen with the escape sequence
\x1b[2J - Moves the cursor to the top-left corner with
\x1b[H - Exits the program with
exit(0)
- Clears the screen with the escape sequence
- If it’s any other character, it does nothing (for now)
Refreshing the Screen
Finally, let’s implement the screen refresh method:
void Editor::refreshScreen() {
// Clear the screen
std::cout << "\x1b[2J"; // Clear the entire screen
std::cout << "\x1b[H"; // Move cursor to top-left corner
// Draw a tilde at the beginning of each line (like vim)
for (int y = 0; y < m_screenrows; y++) {
std::cout << "~\r\n";
}
// Move cursor back to top-left
std::cout << "\x1b[H";
// Make sure output is displayed
std::cout.flush();
}
The refreshScreen() method updates the terminal display:
- It clears the entire screen using the escape sequence
\x1b[2J - It moves the cursor to the top-left corner with
\x1b[H - It draws a tilde (
~) at the beginning of each line to indicate empty lines:- Loops through each row of the screen
- Outputs a tilde followed by a carriage return and newline (
\r\n)
- It moves the cursor back to the top-left corner
- It flushes the output stream to ensure everything is displayed immediately
Putting It All Together
Here’s the complete implementation of src/Editor.cpp:
// src/Editor.cpp
#include "kilo++/Editor.hpp"
#include <iostream>
#include <unistd.h>
#include <sys/ioctl.h>
Editor::Editor() {
// Get terminal size
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) != -1) {
m_screenrows = ws.ws_row;
m_screencols = ws.ws_col;
} else {
// Default size if we can't get the actual size
m_screenrows = 24;
m_screencols = 80;
}
}
void Editor::run() {
while (true) {
refreshScreen();
processKeypress();
}
}
void Editor::processKeypress() {
// Read a character from standard input
char c;
std::cin.get(c);
// Quit if 'q' is pressed
if (c == 'q') {
// Clear the screen before exiting
std::cout << "\x1b[2J";
std::cout << "\x1b[H";
exit(0);
}
}
void Editor::refreshScreen() {
// Clear the screen
std::cout << "\x1b[2J"; // Clear the entire screen
std::cout << "\x1b[H"; // Move cursor to top-left corner
// Draw a tilde at the beginning of each line (like vim)
for (int y = 0; y < m_screenrows; y++) {
std::cout << "~\r\n";
}
// Move cursor back to top-left
std::cout << "\x1b[H";
// Make sure output is displayed
std::cout.flush();
}
Updating Main Function
Now let’s update our main function to use the Editor class. Edit src/kilo.cpp:
// src/kilo.cpp
#include "kilo++/Editor.hpp"
int main() {
Editor editor;
editor.run();
return 0;
}
This main function:
- Creates an instance of our Editor class
- Calls the
run()method to start the editor - Returns 0 when the editor exits (though this won’t happen with our current implementation)
Updating CMakeLists.txt
We need to update our CMakeLists.txt to include our new Editor.cpp file:
cmake_minimum_required(VERSION 3.10)
project(kilo++)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(kilo++
src/kilo.cpp
src/Editor.cpp
)
target_include_directories(kilo++ PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
This CMakeLists.txt:
- Sets the minimum CMake version to 3.10
- Names our project “kilo++”
- Sets the C++ standard to C++17
- Creates an executable from our source files
- Adds our include directory to the include path
Understanding Terminal Control Sequences
The escape sequences we’re using are:
\x1b[2J: Clears the entire screen\x1b[H: Positions the cursor at the top-left corner
These are part of the ANSI escape sequence standard used by most terminal emulators.
Building and Running
Let’s build our project:
cd build
cmake ..
make
Now run the program:
./kilo++
When you run the program, you should see:
- A screen filled with tildes (
~) at the beginning of each line - Press ‘q’ to quit the editor
What We’ve Accomplished
In this chapter, we’ve:
- Created the basic
Editorclass structure - Implemented methods to:
- Initialize the editor and get terminal dimensions
- Process basic keyboard input
- Refresh the screen display
- Set up the main loop structure
- Learned about basic terminal control sequences
In the next chapter, we’ll enhance our editor by:
- Implementing raw mode for better input handling
- Adding a welcome message
- Implementing more sophisticated screen drawing functions
But for now, we have a simple foundation to build upon.