Initial commit

This commit is contained in:
2026-04-27 21:38:06 +10:00
commit e480561fb4
13 changed files with 722 additions and 0 deletions

17
src/buffer.cpp Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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();
};
}