From 5fa3e9e6a15a915be355a8563493ffa3efcf4eee Mon Sep 17 00:00:00 2001 From: Maxwell Jeffress Date: Tue, 7 Oct 2025 19:32:45 +1100 Subject: [PATCH] Initial commit --- README.md | 18 +++ src/main.cpp | 437 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 455 insertions(+) create mode 100644 README.md create mode 100644 src/main.cpp diff --git a/README.md b/README.md new file mode 100644 index 0000000..22eee7e --- /dev/null +++ b/README.md @@ -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 +``` + + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..a9ebdf5 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,437 @@ +#include +#include +#include +#include +#include + +enum class mode { + INSERT, COMMAND, NORMAL +}; + +void quit(int exitcode = 0) { + endwin(); + exit(exitcode); +} + +std::vector readFile(std::string filename) { + std::ifstream file(filename); + if (!file) { + return {""}; + } + std::string buf; + std::vector 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 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 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(); +}