Initial commit
This commit is contained in:
17
src/buffer.cpp
Normal file
17
src/buffer.cpp
Normal file
@@ -0,0 +1,17 @@
|
||||
#include "buffer.hpp"
|
||||
#include <stdexcept>
|
||||
|
||||
namespace Dive {
|
||||
void Buffer::save() {
|
||||
std::ofstream file(fileName);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Cannot open file " + fileName + " for writing");
|
||||
}
|
||||
|
||||
for (const auto& line : lines) {
|
||||
file << line << "\n";
|
||||
}
|
||||
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
40
src/buffer.hpp
Normal file
40
src/buffer.hpp
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <fstream>
|
||||
#include <filesystem>
|
||||
|
||||
namespace Dive {
|
||||
enum class BufferType {
|
||||
File
|
||||
};
|
||||
|
||||
struct Buffer {
|
||||
BufferType type = BufferType::File;
|
||||
std::string fileName = "";
|
||||
std::vector<std::string> lines = {};
|
||||
|
||||
struct {
|
||||
size_t x = 0;
|
||||
size_t y = 0;
|
||||
} pos;
|
||||
|
||||
Buffer(std::string name) : fileName(name) {
|
||||
if (std::filesystem::exists(name)) {
|
||||
std::ifstream file(name);
|
||||
if (!file.is_open()) {
|
||||
throw std::runtime_error("Cannot open file " + name);
|
||||
}
|
||||
std::string buf;
|
||||
while (std::getline(file, buf)) {
|
||||
lines.push_back(buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
Buffer() = default;
|
||||
|
||||
void save();
|
||||
};
|
||||
}
|
||||
252
src/editor.cpp
Normal file
252
src/editor.cpp
Normal file
@@ -0,0 +1,252 @@
|
||||
#include "editor.hpp"
|
||||
#include "renderer.hpp"
|
||||
#include <csignal>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
#include <ncurses.h>
|
||||
|
||||
namespace Dive {
|
||||
void Editor::handleKey(Keycode key) {
|
||||
switch (state) {
|
||||
case EditorState::Normal: {
|
||||
handleKeyNormal(key);
|
||||
break;
|
||||
}
|
||||
case EditorState::Insert: {
|
||||
handleKeyInsert(key);
|
||||
break;
|
||||
}
|
||||
case EditorState::Command: {
|
||||
handleKeyCommand(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::handleKeyNormal(Keycode key) {
|
||||
minibuf.clear();
|
||||
switch (key) {
|
||||
case 'i':
|
||||
{
|
||||
state = EditorState::Insert;
|
||||
break;
|
||||
}
|
||||
case 'a':
|
||||
{
|
||||
state = EditorState::Insert;
|
||||
if (pos.x < buffer.lines[pos.y].size()) {
|
||||
pos.x++;
|
||||
currentLineHangover = pos.x;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'o':
|
||||
{
|
||||
state = EditorState::Insert;
|
||||
buffer.lines.insert(buffer.lines.begin() + pos.y + 1, "");
|
||||
pos.y++;
|
||||
pos.x = 0;
|
||||
currentLineHangover = 0;
|
||||
break;
|
||||
}
|
||||
case ':':
|
||||
case ';':
|
||||
{
|
||||
state = EditorState::Command;
|
||||
break;
|
||||
}
|
||||
|
||||
case KEY_UP:
|
||||
case 'k':
|
||||
{
|
||||
if (pos.y > 0) {
|
||||
pos.y--;
|
||||
if (pos.x > buffer.lines[pos.y].size()) {
|
||||
pos.x = buffer.lines[pos.y].size();
|
||||
} else if (buffer.lines[pos.y].size() < currentLineHangover) {
|
||||
pos.x = buffer.lines[pos.y].size();
|
||||
} else {
|
||||
pos.x = currentLineHangover;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 10:
|
||||
case 13:
|
||||
case KEY_ENTER:
|
||||
case KEY_DOWN:
|
||||
case 'j':
|
||||
{
|
||||
if (pos.y < buffer.lines.size() - 1) {
|
||||
pos.y++;
|
||||
if (pos.x > buffer.lines[pos.y].size()) {
|
||||
pos.x = buffer.lines[pos.y].size();
|
||||
} else if (buffer.lines[pos.y].size() < currentLineHangover) {
|
||||
pos.x = buffer.lines[pos.y].size();
|
||||
} else {
|
||||
pos.x = currentLineHangover;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case KEY_LEFT:
|
||||
case KEY_BACKSPACE:
|
||||
case 'h':
|
||||
{
|
||||
if (pos.x > 0) {
|
||||
pos.x--;
|
||||
currentLineHangover = pos.x;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case KEY_RIGHT:
|
||||
case 'l':
|
||||
{
|
||||
if (pos.x < buffer.lines[pos.y].size()) {
|
||||
pos.x++;
|
||||
currentLineHangover = pos.x;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
void Editor::handleKeyInsert(Keycode key) {
|
||||
switch (key) {
|
||||
case 27: // ESC
|
||||
{
|
||||
state = EditorState::Normal;
|
||||
break;
|
||||
}
|
||||
case KEY_UP:
|
||||
{
|
||||
if (pos.y > 0) {
|
||||
pos.y--;
|
||||
if (pos.x > buffer.lines[pos.y].size()) {
|
||||
pos.x = buffer.lines[pos.y].size();
|
||||
} else if (buffer.lines[pos.y].size() < currentLineHangover) {
|
||||
pos.x = buffer.lines[pos.y].size();
|
||||
} else {
|
||||
pos.x = currentLineHangover;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case KEY_DOWN:
|
||||
{
|
||||
if (pos.y < buffer.lines.size() - 1) {
|
||||
pos.y++;
|
||||
if (pos.x > buffer.lines[pos.y].size()) {
|
||||
pos.x = buffer.lines[pos.y].size();
|
||||
} else if (buffer.lines[pos.y].size() < currentLineHangover) {
|
||||
pos.x = buffer.lines[pos.y].size();
|
||||
} else {
|
||||
pos.x = currentLineHangover;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case KEY_LEFT:
|
||||
{
|
||||
if (pos.x > 0) {
|
||||
pos.x--;
|
||||
currentLineHangover = pos.x;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case KEY_RIGHT:
|
||||
{
|
||||
if (pos.x < buffer.lines[pos.y].size()) {
|
||||
pos.x++;
|
||||
currentLineHangover = pos.x;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case KEY_BACKSPACE:
|
||||
{
|
||||
if (pos.x > 0) {
|
||||
buffer.lines[pos.y].erase(pos.x - 1, 1);
|
||||
pos.x--;
|
||||
currentLineHangover = pos.x;
|
||||
} else {
|
||||
if (pos.y == 0) break;
|
||||
|
||||
// Merge with previous line
|
||||
pos.x = buffer.lines[pos.y - 1].size();
|
||||
currentLineHangover = pos.x;
|
||||
buffer.lines[pos.y - 1] += buffer.lines[pos.y];
|
||||
buffer.lines.erase(buffer.lines.begin() + pos.y);
|
||||
pos.y--;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 10:
|
||||
case 13:
|
||||
case KEY_ENTER:
|
||||
{
|
||||
std::string tail = buffer.lines[pos.y].substr(pos.x);
|
||||
buffer.lines[pos.y] = buffer.lines[pos.y].substr(0, pos.x);
|
||||
buffer.lines.insert(buffer.lines.begin() + pos.y + 1, tail);
|
||||
pos.y++;
|
||||
pos.x = 0;
|
||||
currentLineHangover = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
if (key >= 32 && key < 127) {
|
||||
char ch[2] = { (char)key, '\0' };
|
||||
buffer.lines[pos.y].insert(pos.x, ch);
|
||||
pos.x++;
|
||||
currentLineHangover = pos.x;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Editor::handleKeyCommand(Keycode key) {
|
||||
switch (key) {
|
||||
case 27: // ESC
|
||||
{
|
||||
minibuf.clear();
|
||||
state = EditorState::Normal;
|
||||
break;
|
||||
}
|
||||
case 10:
|
||||
case 13:
|
||||
case KEY_ENTER:
|
||||
{
|
||||
if (minibuf == "q" || minibuf == "quit") {
|
||||
// do this cleaner at some point
|
||||
raise(SIGINT);
|
||||
} else if (minibuf == "w" || minibuf == "write") {
|
||||
try {
|
||||
buffer.save();
|
||||
minibuf = "Wrote to file " + buffer.fileName;
|
||||
} catch (std::exception e) {
|
||||
minibuf = e.what();
|
||||
}
|
||||
} else {
|
||||
minibuf = "Unknown command \"" + minibuf + "\"";
|
||||
}
|
||||
|
||||
state = EditorState::Normal;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
{
|
||||
minibuf += keyname(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/editor.hpp
Normal file
42
src/editor.hpp
Normal file
@@ -0,0 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include "buffer.hpp"
|
||||
#include "renderer.hpp"
|
||||
|
||||
extern std::string diveHome;
|
||||
|
||||
namespace Dive {
|
||||
enum class EditorState {
|
||||
Normal, Insert, Command
|
||||
};
|
||||
|
||||
struct Editor {
|
||||
EditorState state = EditorState::Normal;
|
||||
Buffer buffer;
|
||||
|
||||
struct {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
} pos;
|
||||
|
||||
int currentLineHangover = 0;
|
||||
std::string minibuf = "";
|
||||
|
||||
// Used to store the old position before interacting with the minibuf
|
||||
struct {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
} oldPos;
|
||||
|
||||
Editor(std::string file) : buffer(Buffer(file)) {}
|
||||
Editor() : buffer(Buffer(diveHome)) {}
|
||||
|
||||
void handleKey(Keycode key);
|
||||
|
||||
private:
|
||||
void handleKeyNormal(Keycode key);
|
||||
void handleKeyInsert(Keycode key);
|
||||
void handleKeyCommand(Keycode key);
|
||||
};
|
||||
}
|
||||
|
||||
80
src/main.cpp
Normal file
80
src/main.cpp
Normal file
@@ -0,0 +1,80 @@
|
||||
#include "editor.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
static Dive::Window* _global_window = nullptr;
|
||||
static bool hasCaught = false;
|
||||
|
||||
void exitHandle(int signum) {
|
||||
|
||||
if (hasCaught == true) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
hasCaught = true;
|
||||
|
||||
if (_global_window == nullptr) {
|
||||
return;
|
||||
}
|
||||
_global_window->exit();
|
||||
|
||||
switch (signum) {
|
||||
case SIGABRT:
|
||||
std::cout << "Dive has unexpectedly aborted! (abort signal)\nPlease report this issue on Chookspace (https://chookspace.com/max/dive)" << std::endl;
|
||||
exit(1);
|
||||
|
||||
case SIGSEGV:
|
||||
std::cout << "Dive has unexpectedly aborted! (segfault signal)\nPlease report this issue on Chookspace (https://chookspace.com/max/dive)" << std::endl;
|
||||
exit(1);
|
||||
|
||||
case SIGINT:
|
||||
exit(0);
|
||||
|
||||
default:
|
||||
std::cout << "Dive has unexpectedly aborted! (signal number " << signum << ")\nPlease report this issue on Chookspace (https://chookspace.com/max/dive)" << std::endl;
|
||||
exit(1);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
std::string diveHome = "/.divehome";
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
signal(SIGINT, exitHandle);
|
||||
signal(SIGABRT, exitHandle);
|
||||
signal(SIGSEGV, exitHandle);
|
||||
|
||||
const char* home = getenv("HOME");
|
||||
if (home == NULL) {
|
||||
home = "/";
|
||||
}
|
||||
|
||||
diveHome = home + std::string("/.divehome");
|
||||
|
||||
std::string fileName = diveHome;
|
||||
|
||||
if (argc > 1) {
|
||||
fileName = argv[1];
|
||||
}
|
||||
|
||||
Dive::Editor editor(fileName);
|
||||
if (editor.buffer.lines.empty()) {
|
||||
editor.buffer.lines.push_back("");
|
||||
}
|
||||
Dive::Window window(editor);
|
||||
_global_window = &window;
|
||||
|
||||
try {
|
||||
window.show();
|
||||
} catch (std::exception e) {
|
||||
window.exit();
|
||||
std::cout << "An exception occured in Dive:\n" << e.what() << "\nPlease report this issue on Chookspace (https://chookspace.com/max/dive)" << std::endl;
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
47
src/renderer.cpp
Normal file
47
src/renderer.cpp
Normal file
@@ -0,0 +1,47 @@
|
||||
#include "renderer.hpp"
|
||||
#include <ncurses.h>
|
||||
#include <string>
|
||||
|
||||
namespace Dive {
|
||||
void Renderer::init() {
|
||||
if (!hasInit) {
|
||||
initscr();
|
||||
cbreak();
|
||||
noecho();
|
||||
keypad(stdscr, TRUE);
|
||||
set_escdelay(10);
|
||||
|
||||
hasInit = true;
|
||||
}
|
||||
}
|
||||
|
||||
void Renderer::close() {
|
||||
if (hasInit) endwin();
|
||||
hasInit = false;
|
||||
}
|
||||
|
||||
void Renderer::display() {
|
||||
if (hasInit) refresh();
|
||||
}
|
||||
|
||||
void Renderer::clear() {
|
||||
if (hasInit) erase();
|
||||
}
|
||||
|
||||
Keycode Renderer::wait() {
|
||||
if (hasInit) return getch();
|
||||
else return 0;
|
||||
}
|
||||
|
||||
void Renderer::putString(int x, int y, std::string str) {
|
||||
if (hasInit) mvprintw(y, x, "%s", str.c_str());
|
||||
}
|
||||
|
||||
void Renderer::appendString(std::string str) {
|
||||
if (hasInit) printw("%s", str.c_str());
|
||||
}
|
||||
|
||||
void Renderer::setPos(int x, int y) {
|
||||
if (hasInit) move(y, x);
|
||||
}
|
||||
}
|
||||
85
src/renderer.hpp
Normal file
85
src/renderer.hpp
Normal file
@@ -0,0 +1,85 @@
|
||||
#pragma once
|
||||
|
||||
#include <ncurses.h>
|
||||
#include <string>
|
||||
|
||||
namespace Dive {
|
||||
|
||||
using Keycode = int;
|
||||
|
||||
/*
|
||||
* Renderer struct
|
||||
* Dive's abstraction over ncurses, which allows easy screen manipulation.
|
||||
*
|
||||
* Call Renderer().init() before using any other methods, this will set up the ncurses
|
||||
* screen for you. All other methods will do nothing without the renderer being init'd.
|
||||
*
|
||||
* You don't have to do Renderer().close(), as the destructor will do this for you,
|
||||
* unless you want to exit the window early. The Renderer will keep track of whether
|
||||
* it is init'd, and can be reused after being closed by using init again.
|
||||
*/
|
||||
struct Renderer {
|
||||
|
||||
/*
|
||||
* Initialises ncurses in the terminal. When called, will wipe the terminal and
|
||||
* ensure it is ready for all other functions.
|
||||
*
|
||||
* This method sets up ncurses in the way which Dive expects it to function,
|
||||
* please do not mess with ncurses while the renderer is init'ed.
|
||||
*/
|
||||
void init();
|
||||
|
||||
/*
|
||||
* Closes ncurses in the terminal. Use when you're done. Automatically called by
|
||||
* the destructor.
|
||||
*/
|
||||
void close();
|
||||
|
||||
/*
|
||||
* Displays anything currently in the buffer, waiting to be displayed.
|
||||
*/
|
||||
void display();
|
||||
|
||||
/*
|
||||
* Clears the buffer, ready for new contents.
|
||||
*/
|
||||
void clear();
|
||||
|
||||
/*
|
||||
* Waits for the user to press a key on their keyboard.
|
||||
* This blocks the thread until a key is pressed.
|
||||
*/
|
||||
Keycode wait();
|
||||
|
||||
|
||||
/*
|
||||
* Puts a string into a buffer, at the specified x and y positioning. Moves the
|
||||
* cursor to the needed position.
|
||||
*/
|
||||
void putString(int x, int y, std::string str);
|
||||
|
||||
/*
|
||||
* Puts a string into the buffer at the current position of the cursor. Useful
|
||||
* for status bars.
|
||||
*/
|
||||
void appendString(std::string str);
|
||||
|
||||
/*
|
||||
* Sets the position of the cursor on screen.
|
||||
*/
|
||||
void setPos(int x, int y);
|
||||
|
||||
Renderer() = default;
|
||||
~Renderer() {
|
||||
if (hasInit) close();
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
/*
|
||||
* Ensures that the renderer always knows if ncurses is ready or not.
|
||||
*/
|
||||
bool hasInit = false;
|
||||
|
||||
};
|
||||
}
|
||||
71
src/window.cpp
Normal file
71
src/window.cpp
Normal file
@@ -0,0 +1,71 @@
|
||||
#include "window.hpp"
|
||||
#include "editor.hpp"
|
||||
#include "renderer.hpp"
|
||||
#include <ncurses.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Dive {
|
||||
void Window::show() {
|
||||
renderer.init();
|
||||
|
||||
getmaxyx(stdscr, size.h, size.w);
|
||||
|
||||
int offset = 0;
|
||||
|
||||
for (;;) {
|
||||
|
||||
renderer.clear();
|
||||
|
||||
if (editor.pos.y < offset) {
|
||||
offset = editor.pos.y;
|
||||
} else if (editor.pos.y >= offset + (size.h - 2)) {
|
||||
offset = editor.pos.y - (size.h - 2) + 1;
|
||||
}
|
||||
|
||||
int viewLines = size.h - 2;
|
||||
for (int i = 0; i < viewLines; i++) {
|
||||
int lineIdx = offset + i;
|
||||
if (lineIdx < (int)editor.buffer.lines.size()) {
|
||||
renderer.putString(0, i, editor.buffer.lines[lineIdx]);
|
||||
} else {
|
||||
renderer.putString(0, i, "~");
|
||||
}
|
||||
}
|
||||
|
||||
// Create status bar
|
||||
switch (editor.state) {
|
||||
case EditorState::Normal:
|
||||
renderer.putString(0, size.h - 2, "normal");
|
||||
break;
|
||||
case EditorState::Insert:
|
||||
renderer.putString(0, size.h - 2, "insert");
|
||||
break;
|
||||
case EditorState::Command:
|
||||
renderer.putString(0, size.h - 2, "command");
|
||||
break;
|
||||
}
|
||||
|
||||
// Add minibuf to bottom of screen
|
||||
renderer.putString(0, size.h - 1, editor.minibuf);
|
||||
|
||||
renderer.setPos(editor.pos.x, editor.pos.y - offset);
|
||||
renderer.display();
|
||||
|
||||
Keycode key = renderer.wait();
|
||||
switch (key) {
|
||||
case KEY_RESIZE:
|
||||
getmaxyx(stdscr, size.h, size.w);
|
||||
break;
|
||||
|
||||
default:
|
||||
editor.handleKey(key);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Window::exit() {
|
||||
renderer.close();
|
||||
}
|
||||
}
|
||||
37
src/window.hpp
Normal file
37
src/window.hpp
Normal file
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "editor.hpp"
|
||||
#include "renderer.hpp"
|
||||
|
||||
extern std::string diveHome;
|
||||
|
||||
namespace Dive {
|
||||
|
||||
/*
|
||||
* Window class
|
||||
* Holds information about the current window.
|
||||
*
|
||||
* This is the main class that needs to be interacted with by the main() function.
|
||||
* It handles creating a renderer, keeping track of screen size, and key presses.
|
||||
*/
|
||||
class Window {
|
||||
|
||||
private:
|
||||
Renderer renderer;
|
||||
|
||||
public:
|
||||
Editor editor;
|
||||
|
||||
struct {
|
||||
int w, h;
|
||||
} size;
|
||||
|
||||
int currentBottomLine;
|
||||
|
||||
Window(Editor editor) : editor(editor) {}
|
||||
Window() : editor(Editor(diveHome)) {}
|
||||
|
||||
void show();
|
||||
void exit();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user