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