Initial commit

This commit is contained in:
2025-10-07 19:32:45 +11:00
commit 5fa3e9e6a1
2 changed files with 455 additions and 0 deletions

18
README.md Normal file
View File

@@ -0,0 +1,18 @@
# ve editor
ve is a vi(m) like editor which runs inside your terminal, using ncurses. At present, it is quite simple, with the following features:
* Create and open text files for editing
* Navigate through files with arrow keys (in normal and insert mode) as well as with hjkl (j and k are swapped because I wanted to)
* Enter insert mode with 'i', go back to normal mode with ESC, and enter commands with ':'
* Insert mode works in the way you'd expect
* Scroll through large files
* Saving and quitting in the same way as Vim
## Building
```bash
g++ src/main.cpp -Lncurses -o ve
```

437
src/main.cpp Normal file
View File

@@ -0,0 +1,437 @@
#include <curses.h>
#include <cstdlib>
#include <string>
#include <vector>
#include <fstream>
enum class mode {
INSERT, COMMAND, NORMAL
};
void quit(int exitcode = 0) {
endwin();
exit(exitcode);
}
std::vector<std::string> readFile(std::string filename) {
std::ifstream file(filename);
if (!file) {
return {""};
}
std::string buf;
std::vector<std::string> out;
while (getline(file, buf)) {
out.push_back(buf);
}
return out;
}
void showStatus(std::string status, int code = 0) {
struct {int x = 0; int y = 0;} windowSize;
getmaxyx(stdscr, windowSize.y, windowSize.x);
windowSize.x --;
windowSize.y --;
std::string codeString;
switch (code) {
case 1:
codeString = "Warning: ";
break;
case 2:
codeString = "Error: ";
break;
case 0:
default:
codeString = "Note: ";
break;
}
mvprintw(windowSize.y - 1, 0, (codeString + status).c_str());
clrtoeol();
refresh();
}
int writeFile(std::string filename, std::vector<std::string> content) {
std::ofstream file(filename);
if (file) {
for (const std::string& line : content) {
file << line << "\n";
}
return 0;
} else {
// 1 is for error
return 1;
}
}
int main(int argc, char** argv) {
std::string filename;
std::vector<std::string> lines = {""};
// open a file
if (argc > 1) {
filename = argv[1];
lines = readFile(filename);
if (lines.empty()) {
lines.push_back("");
}
}
// init ncurses and setup
initscr();
cbreak();
noecho();
// use keypad mode
keypad(stdscr, TRUE);
// remember the position of the cursor
struct {int x = 0; int y = 1;} pos;
pos.x = lines[0].size();
// set current mode
mode currMode = mode::NORMAL;
// get window size
struct {int x = 0; int y = 0;} windowSize;
getmaxyx(stdscr, windowSize.y, windowSize.x);
windowSize.x --;
windowSize.y --;
// store the viewpoint of visible lines
struct {int top; int bottom;} viewpoint;
viewpoint.top = 0;
viewpoint.bottom = windowSize.y - 1;
// buffer where we store command
std::string commandbuf;
// status buffers
std::string statusMessage;
int statusCode = 0;
// make sure we keep track of whether we've written to the file
bool fileChanged = false;
// initial rendering before doing anything
// display the current mode at the bottom
switch (currMode) {
case mode::NORMAL:
mvprintw(windowSize.y, 0, "NORMAL");
break;
case mode::COMMAND:
mvprintw(windowSize.y, 0, "COMMAND");
break;
case mode::INSERT:
mvprintw(windowSize.y, 0, "INSERT");
break;
default:
mvprintw(windowSize.y, 0, "Unknown Mode! Press any key to exit");
getch();
quit();
break;
}
// display what's in the buffer
for (size_t i = viewpoint.top; i < lines.size() && i < viewpoint.bottom && (i - viewpoint.top) < windowSize.y; i++) {
mvprintw((i - viewpoint.top) + 1, 0, "%s", lines[i].c_str());
}
// go to where our position is
move(pos.y, pos.x);
// put what's in the buffer on screen
refresh();
// main loop
bool running = true;
while (running) {
// get the key pressed and check what it is
int pressed = getch();
// change behaviour depending on mode
switch (currMode) {
case mode::NORMAL:
{
switch (pressed) {
case 'i':
currMode = mode::INSERT;
break;
case ':':
currMode = mode::COMMAND;
break;
case 'h':
case KEY_LEFT:
if (pos.x > 0) {
pos.x--;
}
break;
case 'j':
case KEY_DOWN:
if (pos.y < lines.size()) {
pos.y++;
if (pos.y - 1 < lines.size() && pos.x > lines[pos.y - 1].size()) {
pos.x = lines[pos.y - 1].size();
}
if (pos.y - 1 >= viewpoint.bottom) {
viewpoint.top++;
viewpoint.bottom++;
}
}
break;
case 'l':
case KEY_RIGHT:
if (pos.y - 1 < lines.size() && pos.x < lines[pos.y - 1].size()) {
pos.x++;
}
break;
case 'k':
case KEY_UP:
if (pos.y > 1) {
pos.y--;
if (pos.x > lines[pos.y - 1].size()) {
pos.x = lines[pos.y - 1].size();
}
if (pos.y - 1 < viewpoint.top) {
viewpoint.top--;
viewpoint.bottom--;
}
}
break;
default:
// do nothing for now
break;
}
break;
}
case mode::INSERT:
{
switch (pressed) {
// escape has been pressed (code 27)
case 27:
currMode = mode::NORMAL;
break;
// backspace has been pressed
// boy there are a lot of ways to say backspace
case KEY_BACKSPACE:
case 127:
case '\b':
{
size_t line_idx = pos.y - 1;
// if at the start of the line, merge this line with the last line
if (pos.x == 0 && line_idx > 0) {
if (!fileChanged) fileChanged = true;
pos.x = lines[line_idx - 1].size();
lines[line_idx - 1] += lines[line_idx];
lines.erase(lines.begin() + line_idx);
pos.y--;
}
// otherwise, delete character before cursor
else if (line_idx >= 0 && line_idx < lines.size() && !lines[line_idx].empty() && pos.x > 0) {
if (!fileChanged) fileChanged = true;
if (pos.x <= lines[line_idx].size()) {
lines[line_idx].erase(pos.x - 1, 1);
}
pos.x--;
}
break;
}
// enter has been pressed (code 10)
case 10:
{
size_t line_idx = pos.y - 1;
if (line_idx < lines.size()) {
// split the line at cursor position
std::string remainder = lines[line_idx].substr(pos.x);
lines[line_idx] = lines[line_idx].substr(0, pos.x);
// insert the remainder as a new line
lines.insert(lines.begin() + line_idx + 1, remainder);
// move to the new line
pos.y++;
pos.x = 0;
if (!fileChanged) fileChanged = true;
}
break;
}
case KEY_LEFT:
if (pos.x > 0) {
pos.x--;
}
break;
case KEY_UP:
if (pos.y > 1) {
pos.y--;
if (pos.x > lines[pos.y - 1].size()) {
pos.x = lines[pos.y - 1].size();
}
if (pos.y - 1 < viewpoint.top) {
viewpoint.top--;
viewpoint.bottom--;
}
}
break;
case KEY_RIGHT:
if (pos.y - 1 < lines.size() && pos.x < lines[pos.y - 1].size()) {
pos.x++;
}
break;
case KEY_DOWN:
if (pos.y < lines.size()) {
pos.y++;
if (pos.y - 1 < lines.size() && pos.x > lines[pos.y - 1].size()) {
pos.x = lines[pos.y - 1].size();
}
if (pos.y - 1 >= viewpoint.bottom) {
viewpoint.top++;
viewpoint.bottom++;
}
}
break;
// otherwise, add the character to the buffer
default:
// make sure it's a printable character
if (pressed >= 32 && pressed <= 126) {
size_t line_idx = pos.y - 1;
if (line_idx >= 0 && line_idx < lines.size()) {
// if we're not at the end, insert
if (pos.x < lines[line_idx].size()) {
lines[line_idx].insert(pos.x, std::string() + char(pressed));
// otherwise, append
} else {
lines[line_idx] += pressed;
}
pos.x++;
if (!fileChanged) fileChanged = true;
}
}
break;
}
break;
}
case mode::COMMAND:
{
switch (pressed) {
// enter key
case 10:
{
// handle commmand entered
if (commandbuf.substr(0, 1) == "q") {
if (fileChanged) {
statusMessage = "File not saved! Try :wq to save and quit, or :q! to forget changes.";
statusCode = 0;
} else {
quit();
}
}
if (commandbuf.substr(0, 1) == "w") {
if (filename.empty()) {
if (commandbuf.size() > 1) {
if (commandbuf[1] == ' ') {
filename = commandbuf.substr(2);
} else {
filename = commandbuf.substr(1);
}
writeFile(filename, lines);
fileChanged = false;
statusMessage = "File written to " + filename;
statusCode = 0;
} else {
statusMessage = "Filename empty! Try :w (filename)";
statusCode = 1;
}
} else {
writeFile(filename, lines);
fileChanged = false;
statusMessage = "File written to " + filename;
statusCode = 0;
}
}
if (commandbuf.substr(0, 2) == "q!") {
quit();
}
if (commandbuf.substr(0, 2) == "wq") {
if (filename.empty()) {
if (commandbuf.size() > 1) {
if (commandbuf[1] == ' ') {
filename = commandbuf.substr(2);
} else {
filename = commandbuf.substr(1);
}
writeFile(filename, lines);
quit();
} else {
statusMessage = "Filename empty! Try :wq (filename)";
statusCode = 1;
}
} else {
writeFile(filename, lines);
quit(0);
}
}
currMode = mode::NORMAL;
commandbuf.clear();
break;
}
default:
// add to the command buffer
commandbuf += pressed;
}
}
}
// erase our buffer ready to rerender
erase();
// display what's in the buffer
for (size_t i = viewpoint.top; i < lines.size() && i < viewpoint.bottom && (i - viewpoint.top) < windowSize.y; i++) {
mvprintw((i - viewpoint.top) + 1, 0, "%s", lines[i].c_str());
}
// display the current mode at the bottom
switch (currMode) {
case mode::NORMAL:
mvprintw(windowSize.y, 0, "NORMAL");
break;
case mode::COMMAND:
mvprintw(windowSize.y, 0, "COMMAND");
mvprintw(windowSize.y - 1, 0, (":" + commandbuf).c_str());
break;
case mode::INSERT:
mvprintw(windowSize.y, 0, "INSERT");
break;
default:
mvprintw(windowSize.y, 0, "Unknown Mode! Press any key to exit");
getch();
quit();
break;
}
// display status message
if (!statusMessage.empty()) {
showStatus(statusMessage, statusCode);
statusMessage.clear();
statusCode = 0;
}
// put the cursor in position
move(pos.y - viewpoint.top, pos.x);
// put what's in the buffer on screen
refresh();
}
quit();
}