mf/subcommand_edit.cpp

151 lines
4.5 KiB
C++

#include <regex>
#include <system_error>
#include <unistd.h>
#include <sys/wait.h>
#include "utils.h"
#include "database.h"
#include "subcommands.h"
using namespace std::string_literals;
const std::regex contents_regex("# Path \\(immutable\\):\n\\s*[^\0]*?\\s*\0\n# Source:\n\\s*([^\0]*?)\\s*\0\n# Description:\n\\s*([^\0]*?)\\s*\0\n# Miscellaneous information:\n\\s*([^\0]*?)\\s*"s);
static inline const char* editor();
static inline std::string read_file(int fd);
struct TemporaryFile {
int fd;
char name[14] = "/tmp/mfXXXXXX";
bool unlinked = false;
TemporaryFile() {
this->fd = mkstemp(this->name);
if (this->fd == -1) {
throw std::system_error(errno, std::generic_category(), "mkstemp()");
}
}
~TemporaryFile() {
if (close(this->fd)) {
perror("close(temp_fd)");
}
this->unlink();
}
void unlink() {
if (!this->unlinked && ::unlink(this->name)) {
perror("unlink(temp_name)");
}
this->unlinked = true;
}
};
void subcommand_edit(const Parser& parser) {
// Sanity check the arguments passed
if (parser.arguments.size() != 2) {
fprintf(stderr, HELP_TEXT, parser.program_name);
exit(1);
}
// Open the database and find the meme's path, relative to the database
Database db = Database::find(false);
std::filesystem::path path = get_path_relative_to_database(parser.arguments[1], db);
// Select the meme, if any
bool meme_found = false;
Sqlite3Statement select_stmt(db.db, "SELECT source, description, miscinfo FROM memes WHERE path = ?");
select_stmt.bind_text(1, path.native(), SQLITE_STATIC);
db.db.exec(select_stmt, [&]() {
meme_found = true;
}, 1);
// Create a temporary file and write the meme's metadata to it
TemporaryFile tempfile;
output_meme(
path.c_str(),
meme_found ? select_stmt.column_text(0) : "",
meme_found ? select_stmt.column_text(1) : "",
meme_found ? select_stmt.column_text(2) : "",
tempfile.fd,
true
);
select_stmt.reset();
select_stmt.bind_null(1);
if (lseek(tempfile.fd, 0, SEEK_SET)) {
perror("lseek(temp_fd)");
exit(1);
}
// Spawn $VISUAL/$EDITOR/vi
pid_t editor_pid = fork();
if (editor_pid == -1) {
perror("fork()");
exit(1);
} else if (editor_pid == 0) {
execlp(editor(), editor(), tempfile.name, nullptr);
perror("execlp()");
_exit(1);
} else if (waitpid(editor_pid, nullptr, 0) != editor_pid) {
perror("waitpid()");
}
// Delete the temporary file and read its contents
tempfile.unlink();
std::string contents = read_file(tempfile.fd);
std::smatch sm;
if (!std::regex_match(contents, sm, contents_regex)) {
fprintf(stderr, "failed to parse file contents\n");
exit(1);
}
// Write new meme metadata to the database
std::string source = sm.str(1);
std::string description = sm.str(2);
std::string miscinfo = sm.str(3);
if (meme_found) {
Sqlite3Statement update_stmt(db.db, "UPDATE memes SET source = ?, description = ?, miscinfo = ? WHERE path = ?");
update_stmt.bind_text(1, source, SQLITE_STATIC);
update_stmt.bind_text(2, description, SQLITE_STATIC);
update_stmt.bind_text(3, miscinfo, SQLITE_STATIC);
update_stmt.bind_text(4, path.native(), SQLITE_STATIC);
db.db.exec(update_stmt);
} else {
Sqlite3Statement insert_stmt(db.db, "INSERT INTO memes (path, source, description, miscinfo) VALUES (?, ?, ?, ?)");
insert_stmt.bind_text(1, path.native(), SQLITE_STATIC);
insert_stmt.bind_text(2, source, SQLITE_STATIC);
insert_stmt.bind_text(3, description, SQLITE_STATIC);
insert_stmt.bind_text(4, miscinfo, SQLITE_STATIC);
db.db.exec(insert_stmt);
}
}
static inline const char* editor() {
if (getenv("VISUAL")) {
return getenv("VISUAL");
}
if (getenv("EDITOR")) {
return getenv("EDITOR");
}
return "vi";
}
static inline std::string read_file(int fd) {
std::string res;
char buf[4096];
while (true) {
ssize_t read_size = read(fd, buf, 4096);
if (read_size == -1) {
throw std::system_error(errno, std::generic_category(), "read(temp_fd)");
} else if (read_size == 0) {
break;
} else {
res.insert(res.end(), buf, &buf[read_size]);
}
}
return res;
}