Initial commit
This commit is contained in:
18
README.md
Normal file
18
README.md
Normal 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
437
src/main.cpp
Normal 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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user