In this example we will make a Vim-style editor, only simpler, but it will include:
Dependencies:
Topics in this article:
delete
, key_enter
and tab key
:Let’s go write the code!
Create project and basic files
mkdir minivim
cd minivim
vim minivim.{hpp,cpp} main.cpp Makefile
minivim.hpp
#pragma once
#include <ncurses.h>
#include <memory>
class MiniVim {
public:
MiniVim();
~MiniVim();
void run();
};
minivim.cpp
#include "minivim.hpp"
MiniVim::MiniVim(){
initscr();
noecho();
cbreak();
keypad(stdscr, true);
}
MiniVim::~MiniVim(){
refresh();
endwin();
}
void MiniVim::run(){
printw("Funcionando!");
getch();
}
main.cpp
#include "minivim.hpp"
int main (int argc, char **argv){
auto minivim = std::make_shared<MiniVim>();
minivim->run();
return 0;
}
Makefile
TARGET=minivim
CXX=g++
DEBUG=-g
OPT=-O0
WARN=-Wall -Wno-unknown-pragmas
NCURSES=-lncurses -ltinfo
CXXFLAGS=$(DEBUG) $(OPT) $(WARN) $(NCURSES) -pipe
LD=g++
LDFLAGS=$(NCURSES)
OBJS= main.o minivim.o
all: $(OBJS)
$(LD) -o $(TARGET) $(OBJS) $(LDFLAGS)
@rm -rf *.o
main.o: main.cpp
$(CXX) -c $(CXXFLAGS) main.cpp -o main.o
minivim.o: minivim.cpp
$(CXX) -c $(CXXFLAGS) minivim.cpp -o minivim.o
Compile and run to test!
make
./minivim
If there is a file it will be the file name, if not it will be untitled
.
minivim.hpp
#include <string>
std::string filename; // protected
MiniVim(const std::string&); // change constructor signature
Add to minivim.cpp
and change the constructor:
MiniVim::MiniVim(const std::string& file){
if(file.empty()){
filename = "untitled";
}else{
filename = file;
}
...
Change the printw
:
printw(&filename[0]);
Change main.cpp
:
#include "minivim.hpp"
int main (int argc, char **argv){
if(argc > 1){
auto minivim = std::make_shared<MiniVim>(argv[1]);
minivim->run();
}else{
auto minivim = std::make_shared<MiniVim>("");
minivim->run();
}
return 0;
}
Compile and run to test!
make
./minivim
./minivim main.cpp
./minivim filename.js
Do it this way temporarily just to see if everything is right
Change minivim.hpp
:
#include <vector>
std::vector<std::string> lines; // private
Change minivim.cpp
:
// add at least one line to not give segmentation fault
lines.push_back(""); // constructor
// COMMENT
//printw(&filename[0]);
//getch();
// RUN
while(getmaxyx(stdscr, LINES, COLS)){
int ch = getch();
std::string s(1, (char)ch);
lines.push_back(s);
for (size_t i {}; i < lines.size(); ++i) {
mvprintw(0, i, lines[i].c_str());
}
}
Compile and run to test!
make
./minivim
Now let’s start writing our MiniVim!
Change minivim.hpp
:
// private
int x, y;
char mode;
std::string status;
// protected
void update();
void statusline();
Change minivim.cpp
, initialize members:
// constructor
x = y = 0;
mode = 'n';
status = "NORMAL";
Create MiniVim::update()
:
void MiniVim::update(){
switch (mode){
case 27:
case 'n':
status = "NORMAL";
break;
case 'i':
status = "INSERT";
break;
case 'q':
break;
}
}
Create MiniVim::statusline()
:
void MiniVim::statusline(){
attron(A_REVERSE);
mvprintw(LINES - 1, 0, status.c_str());
attroff(A_REVERSE);
}
Change MiniVim::run()
:
void MiniVim::run(){
while(mode != 'q'){
update();
statusline();
mode = getch();
}
}
Compile and run to test!
make
./minivim
Manipulate data.
Change minivim.hpp
:
void input(int); // protected
Change minivim.cpp
:
void MiniVim::input(int c){
switch (mode){
case 27:
case 'n':
break;
case 'i':
status = "INSERT";
break;
}
}
Add a new switch
inside case 'n':
switch (c){
case 'q':
mode = 'q';
break;
case 'i':
mode = 'i';
break;
case 'w':
mode = 'w';
break;
}
Add a switch
inside case 'i':
switch (c){
case 27:
mode = 'n';
break;
default:
// TEMP
std::string s(1, c);
lines.push_back(s);
break;
}
Add loop to end of member function:
for (size_t i {}; i < lines.size(); ++i) {
mvprintw(0, i, lines[i].c_str());
}
Change and add to run()
:
int c = getch();
input(c);
Compile and run to test!
make
./minivim
Map keys and coordinates
Change minivim.hpp
:
void print(); // protected
Remove codes from minivim.cpp
and then add in place of push_back()
:
// REMOVE and TEMP
std::string s(1, c);
lines.push_back(s);
for (size_t i {}; i < lines.size(); ++i) {
mvprintw(0, i, lines[i].c_str());
}
// change and add
lines[y].insert(x, 1, c);
++x;
Create content for MiniVim::print()
:
In this way, so that there is no segmentation fault when we create the file opening routine
void MiniVim::print(){
for (size_t i {}; i < LINES - 1; ++i) {
if(i >= lines.size()){
move(i, 0);
clrtoeol();
}else{
mvprintw(i, 0, lines[i].c_str());
}
clrtoeol();
}
move(y, x); // revert
}
Add to run()
:
print(); // right after: statusline();
Compile and run to test!
make
./minivim
Change minivim.hpp
adding member functions:
// PRIVATE
void m_remove(int); // remove character
std::string m_tabs(std::string&); // manipulate m_tabs to remove tabs
void m_insert(std::string, int); // insert in the middle
void m_append(std::string&); // add to end of line
Create execution of all of them:
void MiniVim::m_remove(int number){
lines.erase(lines.begin() + number);
}
std::string MiniVim::m_tabs(std::string& line){
std::size_t tab = line.find('\t');
return tab == line.npos ? line : m_tabs(line.replace(tab, 1, " "));
}
void MiniVim::m_insert(std::string line, int number){
line = m_tabs(line);
lines.insert(lines.begin() + number, line);
}
void MiniVim::m_append(std::string& line){
line = m_tabs(line);
lines.push_back(line);
}
Create a case right after case 'i':
case 'i':
switch (c){
case 27:
mode = 'n';
break;
case 127:
case KEY_BACKSPACE:
if( x == 0 && y > 0){
x = lines[y - 1].length();
lines[y - 1] += lines[y];
m_remove(y);
// ADICIONAR up();
}else if(x > 0){
lines[y].erase(--x, 1);
}
break;
default:
lines[y].insert(x, 1, c);
++x;
break;
}
break;
Change minivim.hpp
adding the member functions:
// protected
void up();
void left();
void right();
void down();
Create content:
void MiniVim::up(){
if(y > 0){
--y;
}
if( x >= lines[y].length()){
x = lines[y].length();
}
move(y, x);
}
void MiniVim::left(){
if(x > 0){
--x;
move(y, x);
}
}
void MiniVim::right(){
if( (int)x <= COLS && x <= lines[y].length() - 1){
++x;
move(y, x);
}
}
void MiniVim::down(){
if( (int)y < LINES && y < lines.size() - 1){
++y;
}
if( x >= lines[y].length()){
x = lines[y].length();
}
move(y, x);
}
Add to start of input(int c)
:
switch (c){
case KEY_UP:
up();
return;
case KEY_LEFT:
left();
return;
case KEY_RIGHT:
right();
return;
case KEY_DOWN:
down();
return;
}
DELETE
, KEY_ENTER
and TAB KEY
:Delete key:
case KEY_DC:
if( x == lines[y].length() && y != lines.size() - 1){
lines[y] += lines[y + 1];
}else{
lines[y].erase(x, 1);
}
break;
default: // before here
Enter key
case KEY_ENTER:
case 10:
if(x < lines[y].length()){
m_insert( lines[y].substr(x, lines[y].length() - x), y + 1 );
lines[y].erase(x, lines[y].length() - x);
}else{
m_insert("", y + 1);
}
x = 0;
down();
break;
default: // before here
Tab key:
case KEY_BTAB:
case KEY_CTAB:
case KEY_STAB:
case KEY_CATAB:
case 9:
lines[y].insert(x, 2, ' ');
x += 2;
break;
default: // before here
Let’s monitor the cursor position and add colors and features!
std::string section
section = {};
update()
function:section = " COLS: " + std::to_string(x) + " | ROWS: " + std::to_string(y) + " | FILE: " + filename;
status.insert(0, " "); // initial SPACE for `status`
Print on statusline()
:
mvprintw( LINES - 1, COLS - section.length(), §ion[0]);
Make the slash long, add to statusline()
right after attron(A_REVERSE);
:
for (int i {}; i < COLS; ++i) {
mvprintw(LINES - 1, i, " ");
}
Add colors, include in the constructor after initscr
and other declarations: use_default_colors();
and in statusline()
:
void MiniVim::statusline(){
start_color();
if( mode == 'n'){
init_pair(1, COLOR_MAGENTA, COLOR_BLACK);
}else{
init_pair(1, COLOR_GREEN, COLOR_BLACK);
}
attron(A_REVERSE);
attron(A_BOLD);
attron(COLOR_PAIR(1));
for (int i {}; i < COLS; ++i) {
mvprintw(LINES - 1, i, " ");
}
mvprintw(LINES - 1, 0, status.c_str());
mvprintw( LINES - 1, COLS - section.length(), §ion[0]);
attroff(COLOR_PAIR(1));
attroff(A_BOLD);
attroff(A_REVERSE);
}
Add to minivim.hpp
:
#include <filesystem>
#include <fstream>
void open(); // protected
Add this code to the constructor before initscr();
:
if(std::filesystem::exists(filename)){
std::ifstream ifile(filename);
if(ifile.is_open()){
while(!ifile.eof()){
std::string buffer;
std::getline(ifile, buffer);
m_append(buffer);
}
}else{
//std::printf("Cannot open file.\n");
throw std::runtime_error("Cannot open file. Permission denied.");
//exit(0);
}
}else{
std::string str {};
m_append(str);
}
To save files first add this member function as protected
to minivim.hpp
:
void save();
Create the run in minivim.cpp
:
void MiniVim::save(){
std::ofstream ofile(filename);
if( ofile.is_open() ){
for (size_t i {}; i < lines.size(); ++i) {
ofile << lines[i];
}
ofile.close();
}else{
refresh();
endwin();
std::printf("Cannot to save file.\n");
exit(0);
}
}
And then change in MiniVim::input(int c)
:
case 'w':
mode = 'w';
save();
break;
Now it’s ready and you can run all the tests!
If you want to see the entire process created here on video, I suggest watching the video below. Despite being in Portuguese, it is possible to follow the step by step regardless of the language.