Skip to content

Instantly share code, notes, and snippets.

@dekrain
Created May 4, 2022 21:34
Show Gist options
  • Save dekrain/2b960c791e60228a3b80e98940b06904 to your computer and use it in GitHub Desktop.
Save dekrain/2b960c791e60228a3b80e98940b06904 to your computer and use it in GitHub Desktop.
Awful little counter management program
/** %pmake **/
/* { "target": "counters", "c++ver": "c++17" } */
/*
Entry with _ means unimplemented, + means implemented.
Controls:
+ ArrowDown = next entry
+ ArrowUp = previous entry
+ ArrowRight = increase counter
+ ArrowLeft = decrease counter
+ Q = quit
+ S = save to file
+ L = load from file
+ N = insert before and switch to edit mode
+ O = insert before empty
+ Enter = switch to edit mode
+ R = remove entry (confirm if not empty)
+ P = make current entry empty (confirm if not empty)
+ PageDown = move entry down
+ PageUp = move entry up
+ ? = safe mode (no external IO)
-BONUS-:
+ / = show help (requires source)
Edit mode / String input:
+ Enter = exit edit mode
+ ArrowRight = move forward
+ ArrowLeft = move backward
+ Home = move to start
+ End = move to end
+ Backspace = remove previous character
+ Delete = remove current character
+ [char] = insert character
*/
#include <cstdlib>
#include <cstdio>
#include <cstdint>
#include <cctype>
#include <cinttypes>
#include <cstring>
#include <string>
#include <vector>
#include <charconv>
#include <signal.h>
#include <termios.h>
using zstring = char const*;
struct string_buffer : std::string {
using std::string::string;
void copy(size_t dst, size_t src, size_t count) {
std::memmove(data() + dst, data() + src, count * sizeof(value_type));
}
void copy(size_t dst, std::string_view view) {
std::memcpy(data() + dst, view.data(), view.size() * sizeof(value_type));
}
};
static struct termios s_old_mode;
void disableRawMode(void) {
tcsetattr(STDIN_FILENO, TCSAFLUSH, &s_old_mode);
}
void enableRawMode(void) {
tcgetattr(STDIN_FILENO, &s_old_mode);
struct termios raw = s_old_mode;
raw.c_lflag &= ~(ECHO | ICANON | ISIG);
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
std::atexit(disableRawMode);
}
/**
* Try to read one line from file
* @param fin File to read from
* @param line String that will contain the result
* @returns true on success, false on failure, e.g. end of file
*/
bool betterReadLine(std::FILE* fin, std::string& line) {
line.clear();
line.reserve(0x20);
int ch;
for (ch = std::fgetc(fin); ch != '\n' and ch != EOF; ch = std::fgetc(fin)) {
line += static_cast<char>(ch);
}
return ch != EOF;
}
struct Counter {
std::string name;
int32_t value;
Counter() : value(0) {}
Counter(std::string&& name, int32_t init = 0) : name(std::move(name)), value(init) {}
Counter(Counter&&) = default;
explicit Counter(Counter const&) = default;
Counter& operator=(Counter&&) = default;
explicit operator bool() const { return !name.empty(); }
};
std::vector<Counter> gCounters;
uint32_t gCurrentEntry = 0;
bool currentActive() {
return gCurrentEntry < gCounters.size() and gCounters[gCurrentEntry];
}
std::string gLastFilename;
bool gSafeMode = false;
static char gErrorBuf[0x200];
static char const cErrorOpenFile[] = "Unable to open file";
static char const cErrorSafeMode[] = "Safe mode is enabled";
static char const cErrorNoHelp[] = "No help file (counters.cpp) available";
char const* writeError(int errnum, char const* error, size_t errorLen) {
std::memcpy(gErrorBuf, error, errorLen);
size_t errPos = errorLen;
gErrorBuf[errPos++] = ':';
gErrorBuf[errPos++] = ' ';
strerror_r(errnum, gErrorBuf, sizeof gErrorBuf - errPos);
return gErrorBuf;
}
void writeString(std::string_view text, std::FILE* f = stdout) {
std::fwrite(text.data(), sizeof(std::string_view::value_type), text.size(), f);
}
void loadCountersFromFile(zstring fname, void (*error)(zstring text)) {
if (gSafeMode) {
error(cErrorSafeMode);
return;
}
std::FILE* f = std::fopen(fname, "r");
if (!f) {
error(writeError(errno, cErrorOpenFile, sizeof cErrorOpenFile - 1));
return;
}
std::string line;
while (betterReadLine(f, line)) {
auto sep = line.find('=');
int32_t count = 0;
if (sep != std::string::npos) {
auto num = sep + 1;
while (num < line.size() and std::isblank(line[num]))
++num;
auto pos = std::from_chars(line.data() + num, line.data() + line.size(), count);
if (pos.ec != std::errc()) {
std::fputs("Bad number format\n", stderr);
std::exit(1);
}
while (sep > 0 and std::isblank(line[sep-1]))
--sep;
line.erase(sep);
}
gCounters.emplace_back(std::move(line), count);
}
}
void saveCountersToFile(zstring fname, void (*error)(zstring text)) {
if (gSafeMode) {
error(cErrorSafeMode);
return;
}
std::FILE* f = std::fopen(fname, "w");
if (!f) {
error(writeError(errno, cErrorOpenFile, sizeof cErrorOpenFile - 1));
return;
}
for (auto const& counter : gCounters) {
if (!counter) {
std::fputc('\n', f);
continue;
}
writeString(counter.name, f);
std::fprintf(f, " = %" PRId32 "\n", counter.value);
}
}
void moveCursorTo(uint32_t x, uint32_t y) {
std::printf("\e[%" PRIu32 ";%" PRIu32 "H", y + 1, x + 1);
std::fflush(stdout);
}
void getCursorPosition(uint32_t& x, uint32_t& y) {
std::fputs("\e[6n", stdout);
std::fflush(stdout);
while (std::fgetc(stdin) != 0x1B)
;
if (std::fgetc(stdin) != '[')
std::exit(2);
char buf[16];
char* p = buf;
while (p < std::end(buf) and (*p = std::fgetc(stdin)) != 'R')
++p;
if (p == std::end(buf))
std::exit(2);
char* sep = reinterpret_cast<char*>(std::memchr(buf, ';', p - buf));
if (!sep)
std::exit(2);
auto res = std::from_chars(buf, sep, y);
if (res.ec != std::errc() or res.ptr != sep)
std::exit(2);
--y;
res = std::from_chars(sep + 1, p, x);
if (res.ec != std::errc() or res.ptr != p)
std::exit(2);
--x;
}
void clearLine() {
std::fputs("\e[2K", stdout);
std::fflush(stdout);
}
void clearLineForward() {
std::fputs("\e[0K", stdout);
std::fflush(stdout);
}
void clearScreen() {
std::fputs("\e[H\e[2J", stdout);
std::fflush(stdout);
}
void updateCounter(uint32_t idx) {
auto& counter = gCounters[idx];
moveCursorTo(0, idx);
clearLine();
if (counter) {
std::fwrite(counter.name.data(), 1, counter.name.size(), stdout);
std::printf(" = %" PRId32, counter.value);
}
}
void updateEntryCursor() {
if (currentActive()) {
// Put the cursor on '='
moveCursorTo(gCounters[gCurrentEntry].name.size() + 1, gCurrentEntry);
} else {
// Put on the start
moveCursorTo(0, gCurrentEntry);
}
}
void updateEntries() {
for (uint32_t idx = 0, cnt = gCounters.size(); idx != cnt; ++idx)
updateCounter(idx);
updateEntryCursor();
}
void errorTuiStart(char const* text) {
moveCursorTo(0, gCounters.size());
std::fputs(text, stdout);
}
void errorTuiWait() {
std::fflush(stdout);
std::fgetc(stdin);
clearLine();
updateEntryCursor();
}
void errorTui(char const* text) {
errorTuiStart(text);
errorTuiWait();
}
bool confirmAction() {
moveCursorTo(0, gCounters.size());
std::fputs("Confirm action? [Ny] ", stdout);
uint8_t res = 0;
do switch (std::fgetc(stdin)) {
case 'N':
case 'n':
case 0x0A:
res = 1;
break;
case 'Y':
case 'y':
res = 2;
break;
} while (!res);
clearLine();
updateEntryCursor();
return res == 2;
}
void showHelp() {
std::FILE* f = std::fopen("counters.cpp", "r");
if (!f) {
errorTui(cErrorNoHelp);
return;
}
std::string line;
enum {
SEnd,
SSearch,
SSection,
SEntry,
} state = SSearch;
while (state != SEnd and betterReadLine(f, line)) {
if (line.size() and line.back() == '\n')
line.pop_back();
lReparse:
switch (state) {
case SEnd:
break;
case SSearch:
if (line == "/*") {
state = SSection;
clearScreen();
}
break;
case SSection:
if (line == "*/") {
lEnd:
state = SEnd;
break;
}
if (!line.empty()) {
std::fputs("\e[96m", stdout);
std::fwrite(line.data(), 1, line.size(), stdout);
if (line.back() == ':')
state = SEntry;
}
lNl:
std::fputc('\n', stdout);
break;
case SEntry:
if (line.empty()) {
state = SSection;
goto lNl;
}
if (line.front() == '+') {
std::fputs("\e[92m", stdout);
} else if (line.front() == '_') {
std::fputs("\e[91m", stdout);
} else {
state = SSection;
goto lReparse;
}
std::fputc(line.front(), stdout);
size_t pos = 1, sep;
while (pos < line.size() and std::isblank(line[pos]))
++pos;
sep = line.find_first_of(" \t", pos);
if (sep != std::string::npos) {
std::fputs(" \e[93m", stdout);
std::fwrite(line.data() + pos, 1, sep - pos, stdout);
while (sep < line.size() and std::isblank(line[sep]))
++sep;
} else
sep = pos;
std::fputs(" \e[97m", stdout);
std::fwrite(line.data() + sep, 1, line.size() - sep, stdout);
goto lNl;
}
}
if (state == SSearch) {
errorTui(cErrorNoHelp);
return;
}
std::fgetc(stdin);
std::fputs("\e[0m", stdout);
clearScreen();
updateEntries();
}
std::string inputString(zstring prompt, std::string_view init={});
void insertOne() {
gCounters.insert(gCounters.begin() + gCurrentEntry, Counter{});
updateEntries();
}
void removeEntry() {
if (gCurrentEntry >= gCounters.size())
return;
if (currentActive() and !confirmAction())
return;
gCounters.erase(gCounters.begin() + gCurrentEntry);
// Update view and clear last line
updateEntries();
moveCursorTo(0, gCounters.size());
clearLine();
updateEntryCursor();
}
void blankEntry() {
if (!currentActive() or !confirmAction())
return;
gCounters[gCurrentEntry] = {};
updateCounter(gCurrentEntry);
}
void moveEntry(int32_t dir) {
// if (!dir)
// return;
if (gCurrentEntry >= gCounters.size())
return;
int32_t targetEntry = gCurrentEntry + dir;
if (targetEntry < 0 or targetEntry >= gCounters.size())
return;
std::swap(gCounters[gCurrentEntry], gCounters[targetEntry]);
updateCounter(gCurrentEntry);
updateCounter(targetEntry);
gCurrentEntry = targetEntry;
updateEntryCursor();
}
void loadFromFile() {
gLastFilename = inputString("File name to load: ", gLastFilename);
gCounters.clear();
gCurrentEntry = 0;
loadCountersFromFile(gLastFilename.c_str(), errorTui);
updateEntries();
}
void saveToFile() {
gLastFilename = inputString("File name to save: ", gLastFilename);
saveCountersToFile(gLastFilename.c_str(), errorTui);
}
enum class SpecialChar : int16_t {
ArrowUp = -1,
ArrowDown = -2,
ArrowRight = -3,
ArrowLeft = -4,
Backspace = -5,
Delete = -6,
Enter = -7,
Home = -8,
End = -9,
PageUp = -10,
PageDown = -11,
};
struct InputChar {
int16_t value;
constexpr operator int16_t() const && { return value; }
constexpr operator int16_t&() & { return value; }
constexpr operator int16_t const&() const & { return value; }
constexpr InputChar(int16_t v = 0) : value(v) {}
constexpr InputChar(char v) : value((int16_t)(uint8_t)v) {}
constexpr InputChar(SpecialChar v) : value((int16_t)v) {}
};
struct InputEntry {
InputChar ch;
void (*fun)();
};
extern InputEntry const gModeNormal[];
extern InputEntry const gModeEdit[];
InputEntry const* gMode = gModeNormal;
InputChar gInputChar;
void handleInput();
void enterEditMode();
InputEntry const gModeNormal[] {
{SpecialChar::ArrowUp, [] { if (gCurrentEntry == 0) gCurrentEntry = gCounters.size(); else --gCurrentEntry; updateEntryCursor(); }},
{SpecialChar::ArrowDown, [] { if (gCurrentEntry == gCounters.size()) gCurrentEntry = 0; else ++gCurrentEntry; updateEntryCursor(); }},
{SpecialChar::ArrowRight, [] { if (currentActive()) ++gCounters[gCurrentEntry].value; updateCounter(gCurrentEntry); }},
{SpecialChar::ArrowLeft, [] { if (currentActive()) --gCounters[gCurrentEntry].value; updateCounter(gCurrentEntry); }},
{'q', [] { gMode = nullptr; }},
{'s', saveToFile},
{'l', loadFromFile},
{'n', [] { insertOne(); enterEditMode(); }},
{'o', [] { insertOne(); }},
{SpecialChar::Enter, [] { enterEditMode(); moveCursorTo(0, gCurrentEntry); }},
{'r', [] { removeEntry(); }},
{'p', [] { blankEntry(); }},
{SpecialChar::PageUp, [] { moveEntry(-1); }},
{SpecialChar::PageDown, [] { moveEntry(1); }},
{'?', [] { if (!confirmAction()) return; gSafeMode = true; errorTui("Entered safe mode"); }},
{'/', showHelp},
{}
};
struct EditContext {
std::string suffix;
string_buffer buf;
uint32_t pos, len, xpos, ypos;
bool (*onInsertCharacter)(char ch);
void (*onExit)(std::string&& res);
explicit operator bool() const { return len == ~static_cast<uint32_t>(0); }
void open(uint32_t x, uint32_t y, std::string_view init, std::string suffix_={}) {
suffix = std::move(suffix_);
pos = 0;
xpos = x;
ypos = y;
len = init.size();
buf.resize((15 + len) & ~15);
buf.copy(0, init);
moveCursorTo(xpos, ypos);
clearLineForward();
bool moved = false;
if (!init.empty()) {
writeString(init);
moved = true;
}
if (!suffix.empty()) {
writeString(suffix);
moved = true;
}
if (moved)
moveCursorTo(xpos, ypos);
}
void close() {
if (!*this)
return;
pos = xpos = ypos = 0;
len = ~static_cast<uint32_t>(0);
buf.clear();
suffix.clear();
onInsertCharacter = nullptr;
onExit = nullptr;
}
void exit() {
buf.resize(len);
onExit(std::move(buf));
close();
}
void grow(uint32_t delta) {
buf.reserve(len += delta);
if (buf.capacity() != buf.size())
buf.resize(buf.capacity());
}
} editContext;
void enterEditMode() {
if (editContext)
return;
if (gCurrentEntry >= gCounters.size()) {
gCurrentEntry = gCounters.size();
gCounters.push_back(Counter{});
}
Counter& cnt = gCounters[gCurrentEntry];
std::string suffix;
if (cnt) {
suffix.append(" = ").append(std::to_string(cnt.value));
}
editContext.open(0, gCurrentEntry, cnt.name, std::move(suffix));
editContext.onExit = [] (std::string&& name) {
gMode = gModeNormal;
if (!gCounters[gCurrentEntry])
gCounters[gCurrentEntry].value = 0;
gCounters[gCurrentEntry].name = std::move(name);
updateCounter(gCurrentEntry);
};
editContext.onInsertCharacter = [] (char ch) {
return ch != '=';
};
gMode = gModeEdit;
}
std::string inputString(zstring prompt, std::string_view init) {
if (editContext)
std::abort();
moveCursorTo(0, gCounters.size());
std::fputs(prompt, stdout);
std::fflush(stdout);
uint32_t x, y;
getCursorPosition(x, y);
static std::string res;
InputEntry const* prevMode = gMode;
res.clear();
editContext.open(x, y, init);
editContext.onExit = [] (std::string&& r) { gMode = nullptr; res = std::move(r); };
gMode = gModeEdit;
while (gMode)
handleInput();
gMode = prevMode;
clearLine();
updateEntryCursor();
return std::move(res);
}
void editCursor() {
moveCursorTo(editContext.xpos + editContext.pos, editContext.ypos);
}
void editPaint() {
moveCursorTo(editContext.xpos, editContext.ypos);
clearLineForward();
std::fwrite(editContext.buf.data(), sizeof(string_buffer::value_type), editContext.len, stdout);
if (!editContext.suffix.empty())
std::fwrite(editContext.suffix.data(), sizeof(std::string::value_type), editContext.suffix.size(), stdout);
moveCursorTo(editContext.xpos + editContext.pos, editContext.ypos);
}
void editRight() {
if (editContext.pos >= editContext.len)
return;
++editContext.pos;
editCursor();
}
void editLeft() {
if (editContext.pos == 0)
return;
--editContext.pos;
editCursor();
}
void editHome() {
if (editContext.pos == 0)
return;
editContext.pos = 0;
editCursor();
}
void editEnd() {
if (editContext.pos == editContext.len)
return;
editContext.pos = editContext.len;
editCursor();
}
void editRemovePrevious() {
if (editContext.pos == 0)
return;
editContext.buf.copy(editContext.pos - 1, editContext.pos, editContext.len - editContext.pos);
--editContext.pos;
--editContext.len;
editPaint();
}
void editRemoveCurrent() {
if (editContext.pos == editContext.len)
return;
editContext.buf.copy(editContext.pos, editContext.pos + 1, editContext.len - editContext.pos - 1);
--editContext.len;
editPaint();
}
void editInsertCharacter() {
// Ignore non-text characters
if (gInputChar < 0x20 or gInputChar >= 0x7F)
return;
if (editContext.onInsertCharacter and not editContext.onInsertCharacter(gInputChar))
return;
// [012_345]
// pos = 3; len = 6
editContext.grow(1);
// [012_345%]
++editContext.pos;
// [0123_45%]
// pos = 4; len = 7
editContext.buf.copy(editContext.pos, editContext.pos - 1, editContext.len - editContext.pos);
// [0123_345]
editContext.buf[editContext.pos - 1] = gInputChar;
// [012I_345]
// pos = 4
editPaint();
}
InputEntry const gModeEdit[] {
{SpecialChar::Enter, [] { editContext.exit(); }},
{SpecialChar::ArrowRight, editRight},
{SpecialChar::ArrowLeft, editLeft},
{SpecialChar::Home, editHome},
{SpecialChar::End, editEnd},
{SpecialChar::Backspace, editRemovePrevious},
{SpecialChar::Delete, editRemoveCurrent},
{{}, editInsertCharacter}
};
void handleInput(InputChar character) {
if (!gMode)
return;
gInputChar = character;
InputEntry const* entry;
for (entry = gMode; entry->ch; ++entry) {
if (entry->ch != character)
continue;
if (entry->fun)
entry->fun();
return;
}
if (entry->fun)
entry->fun();
}
bool isescterm(uint8_t ch) {
return ch >= 0x40 and ch <= 0x7E;
}
void handleInput() {
SpecialChar spech {};
int ch = std::fgetc(stdin);
if (ch == EOF or ch == 0x03 /* ^C */) {
gMode = nullptr;
return;
}
if (ch != 0x1B) {
switch (ch) {
case 0x7F:
return handleInput(SpecialChar::Backspace);
case 0x0A:
return handleInput(SpecialChar::Enter);
default:
return handleInput((char)ch);
}
}
char buf[16];
char* p = buf;
// Handle '[' as start seperately
*p++ = std::fgetc(stdin);
while (p != std::end(buf) and !isescterm(*p = std::fgetc(stdin)))
++p;
if (p != std::end(buf))
ch = *p;
else do
ch = std::fgetc(stdin);
while (!isescterm(ch));
// Check known sequences
if (buf[0] == '[' and p - buf >= 1) {
switch (buf[1]) {
case 'A':
spech = SpecialChar::ArrowUp; break;
case 'B':
spech = SpecialChar::ArrowDown; break;
case 'C':
spech = SpecialChar::ArrowRight; break;
case 'D':
spech = SpecialChar::ArrowLeft; break;
case 'F':
spech = SpecialChar::End; break;
case 'H':
spech = SpecialChar::Home; break;
}
if (spech != SpecialChar{})
return handleInput(spech);
if (ch == '~') {
uint32_t idx = 0;
if (p - buf >= 2 and std::isdigit(buf[1])) {
idx = buf[1] - '0';
if (p - buf >= 3 and std::isdigit(buf[2])) {
idx *= 10;
idx += buf[2] - '0';
if (p - buf >= 4)
idx = -1;
}
}
switch (idx) {
case 1:
case 7:
spech = SpecialChar::Home; break;
case 3:
spech = SpecialChar::Delete; break;
case 4:
case 8:
spech = SpecialChar::End; break;
case 5:
spech = SpecialChar::PageUp; break;
case 6:
spech = SpecialChar::PageDown; break;
}
if (spech != SpecialChar{})
return handleInput(spech);
}
}
errorTuiStart("Unknown escape sequence: \"");
for (zstring rp = buf; rp <= p and rp != std::end(buf); ++rp) {
uint8_t ch = *rp;
if (ch >= 0x20 and ch <= 0x7E and ch != '"')
std::fputc(ch, stdout);
else
std::printf("\\x%02" PRIX8, ch);
}
if (p == std::end(buf))
std::fputs("...", stdout);
std::fputc('"', stdout);
errorTuiWait();
}
void errorExit(char const* text) {
std::fputs(text, stderr);
std::exit(1);
}
int main(int argc, char** argv) {
enableRawMode();
if (argc >= 2) {
gLastFilename = argv[1];
loadCountersFromFile(argv[1], errorExit);
}
clearScreen();
updateEntries();
while (gMode)
handleInput();
moveCursorTo(0, gCounters.size());
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment