mirror of
https://github.com/Manoj-HV30/keystats.git
synced 2026-05-16 19:35:23 +00:00
first commit - mai san
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
build/
|
||||
*.db
|
||||
*.o
|
||||
*.out
|
||||
.cache/
|
||||
compile_commands.json
|
||||
@@ -0,0 +1,22 @@
|
||||
cmake_minimum_required(VERSION 3.15)
|
||||
project(keystroke-counter)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
|
||||
include(FetchContent)
|
||||
FetchContent_Declare(ftxui
|
||||
GIT_REPOSITORY https://github.com/ArthurSonzogni/FTXUI
|
||||
GIT_TAG main
|
||||
)
|
||||
FetchContent_MakeAvailable(ftxui)
|
||||
|
||||
add_executable(keystroke main.cpp)
|
||||
target_link_libraries(keystroke sqlite3)
|
||||
|
||||
add_executable(keystroke-stats stats.cpp)
|
||||
target_link_libraries(keystroke-stats
|
||||
sqlite3
|
||||
ftxui::screen
|
||||
ftxui::dom
|
||||
ftxui::component
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
PREFIX ?= /usr/local
|
||||
BINARY_DAEMON = keystroke
|
||||
BINARY_STATS = keystroke-stats
|
||||
SERVICE = keystats.service
|
||||
SYSTEMD_USER = /usr/lib/systemd/user
|
||||
|
||||
.PHONY: all build install uninstall status logs clean
|
||||
|
||||
all: build
|
||||
|
||||
build:
|
||||
cmake -S . -B build
|
||||
cmake --build build
|
||||
|
||||
install: build
|
||||
sudo cp build/$(BINARY_DAEMON) $(PREFIX)/bin/$(BINARY_DAEMON)
|
||||
sudo cp build/$(BINARY_STATS) $(PREFIX)/bin/$(BINARY_STATS)
|
||||
sudo cp $(SERVICE) $(SYSTEMD_USER)/$(SERVICE)
|
||||
|
||||
uninstall:
|
||||
systemctl --user stop $(BINARY_DAEMON) || true
|
||||
systemctl --user disable $(BINARY_DAEMON) || true
|
||||
sudo rm -f $(SYSTEMD_USER)/$(SERVICE)
|
||||
sudo rm -f $(PREFIX)/bin/$(BINARY_DAEMON)
|
||||
sudo rm -f $(PREFIX)/bin/$(BINARY_STATS)
|
||||
|
||||
status:
|
||||
systemctl --user status $(BINARY_DAEMON)
|
||||
|
||||
logs:
|
||||
journalctl --user -u $(BINARY_DAEMON) -f
|
||||
|
||||
clean:
|
||||
rm -rf build
|
||||
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Keystats - Per-key keystroke counter
|
||||
After=graphical-session.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
ExecStart=/usr/local/bin/keystroke
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,240 @@
|
||||
#include <iostream>
|
||||
#include <unordered_map>
|
||||
#include <string>
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <dirent.h>
|
||||
#include <pwd.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <linux/input.h>
|
||||
#include <linux/input-event-codes.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sqlite3.h>
|
||||
|
||||
// ─── Key name map ─────────────────────────────────────────────────────────────
|
||||
|
||||
std::unordered_map<int, std::string> keyNames = {
|
||||
{KEY_ESC, "Esc"}, {KEY_F1, "F1"}, {KEY_F2, "F2"}, {KEY_F3, "F3"},
|
||||
{KEY_F4, "F4"}, {KEY_F5, "F5"}, {KEY_F6, "F6"}, {KEY_F7, "F7"},
|
||||
{KEY_F8, "F8"}, {KEY_F9, "F9"}, {KEY_F10, "F10"}, {KEY_F11, "F11"},
|
||||
{KEY_F12, "F12"},
|
||||
{KEY_GRAVE, "`"}, {KEY_1, "1"}, {KEY_2, "2"}, {KEY_3, "3"},
|
||||
{KEY_4, "4"}, {KEY_5, "5"}, {KEY_6, "6"}, {KEY_7, "7"},
|
||||
{KEY_8, "8"}, {KEY_9, "9"}, {KEY_0, "0"}, {KEY_MINUS, "-"},
|
||||
{KEY_EQUAL, "="}, {KEY_BACKSPACE, "Backspace"},
|
||||
{KEY_TAB, "Tab"}, {KEY_Q, "Q"}, {KEY_W, "W"}, {KEY_E, "E"},
|
||||
{KEY_R, "R"}, {KEY_T, "T"}, {KEY_Y, "Y"}, {KEY_U, "U"},
|
||||
{KEY_I, "I"}, {KEY_O, "O"}, {KEY_P, "P"}, {KEY_LEFTBRACE, "["},
|
||||
{KEY_RIGHTBRACE, "]"}, {KEY_BACKSLASH, "\\"},
|
||||
{KEY_CAPSLOCK, "CapsLock"}, {KEY_A, "A"}, {KEY_S, "S"}, {KEY_D, "D"},
|
||||
{KEY_F, "F"}, {KEY_G, "G"}, {KEY_H, "H"}, {KEY_J, "J"},
|
||||
{KEY_K, "K"}, {KEY_L, "L"}, {KEY_SEMICOLON, ";"}, {KEY_APOSTROPHE, "'"},
|
||||
{KEY_ENTER, "Enter"},
|
||||
{KEY_LEFTSHIFT, "LShift"}, {KEY_Z, "Z"}, {KEY_X, "X"}, {KEY_C, "C"},
|
||||
{KEY_V, "V"}, {KEY_B, "B"}, {KEY_N, "N"}, {KEY_M, "M"},
|
||||
{KEY_COMMA, ","}, {KEY_DOT, "."}, {KEY_SLASH, "/"}, {KEY_RIGHTSHIFT, "RShift"},
|
||||
{KEY_LEFTCTRL, "LCtrl"}, {KEY_LEFTMETA, "Super"}, {KEY_LEFTALT, "LAlt"},
|
||||
{KEY_SPACE, "Space"}, {KEY_RIGHTALT, "RAlt"}, {KEY_RIGHTMETA, "RSuper"},
|
||||
{KEY_RIGHTCTRL, "RCtrl"},
|
||||
{KEY_INSERT, "Insert"}, {KEY_DELETE, "Delete"}, {KEY_HOME, "Home"},
|
||||
{KEY_END, "End"}, {KEY_PAGEUP, "PgUp"}, {KEY_PAGEDOWN, "PgDn"},
|
||||
{KEY_UP, "Up"}, {KEY_DOWN, "Down"}, {KEY_LEFT, "Left"}, {KEY_RIGHT, "Right"},
|
||||
{KEY_NUMLOCK, "NumLock"}, {KEY_KPSLASH, "KP/"}, {KEY_KPASTERISK, "KP*"},
|
||||
{KEY_KPMINUS, "KP-"}, {KEY_KP7, "KP7"}, {KEY_KP8, "KP8"},
|
||||
{KEY_KP9, "KP9"}, {KEY_KPPLUS, "KP+"}, {KEY_KP4, "KP4"},
|
||||
{KEY_KP5, "KP5"}, {KEY_KP6, "KP6"}, {KEY_KP1, "KP1"},
|
||||
{KEY_KP2, "KP2"}, {KEY_KP3, "KP3"}, {KEY_KP0, "KP0"},
|
||||
{KEY_KPDOT, "KP."}, {KEY_KPENTER, "KPEnter"},
|
||||
};
|
||||
|
||||
// ─── Globals ──────────────────────────────────────────────────────────────────
|
||||
|
||||
std::unordered_map<int, long long> keyCounts;
|
||||
sqlite3* db = nullptr;
|
||||
int fd = -1;
|
||||
|
||||
// ─── DB ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
void initDB() {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
std::string dbPath = std::string(pw->pw_dir) + "/.keystroke_counts.db";
|
||||
|
||||
if (sqlite3_open(dbPath.c_str(), &db) != SQLITE_OK) {
|
||||
std::cerr << "Failed to open DB: " << sqlite3_errmsg(db) << "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
const char* sql =
|
||||
"CREATE TABLE IF NOT EXISTS key_counts ("
|
||||
" key_code INTEGER PRIMARY KEY,"
|
||||
" key_name TEXT,"
|
||||
" count INTEGER DEFAULT 0"
|
||||
");";
|
||||
sqlite3_exec(db, sql, nullptr, nullptr, nullptr);
|
||||
|
||||
const char* sql2 =
|
||||
"CREATE TABLE IF NOT EXISTS daily_counts ("
|
||||
" date TEXT,"
|
||||
" key_code INTEGER,"
|
||||
" key_name TEXT,"
|
||||
" count INTEGER DEFAULT 0,"
|
||||
" PRIMARY KEY (date, key_code)"
|
||||
");";
|
||||
sqlite3_exec(db, sql2, nullptr, nullptr, nullptr);
|
||||
}
|
||||
|
||||
void loadFromDB() {
|
||||
const char* sql = "SELECT key_code, count FROM key_counts;";
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
int code = sqlite3_column_int(stmt, 0);
|
||||
long long cnt = sqlite3_column_int64(stmt, 1);
|
||||
keyCounts[code] = cnt;
|
||||
}
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
|
||||
void flushToDB() {
|
||||
sqlite3_exec(db, "BEGIN TRANSACTION;", nullptr, nullptr, nullptr);
|
||||
|
||||
const char* sql =
|
||||
"INSERT INTO key_counts (key_code, key_name, count) VALUES (?, ?, ?)"
|
||||
" ON CONFLICT(key_code) DO UPDATE SET count = excluded.count;";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
|
||||
|
||||
for (auto& [code, count] : keyCounts) {
|
||||
std::string name = keyNames.count(code)
|
||||
? keyNames[code]
|
||||
: "Unknown(" + std::to_string(code) + ")";
|
||||
|
||||
sqlite3_bind_int(stmt, 1, code);
|
||||
sqlite3_bind_text(stmt, 2, name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int64(stmt, 3, count);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_reset(stmt);
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_exec(db, "COMMIT;", nullptr, nullptr, nullptr);
|
||||
}
|
||||
|
||||
|
||||
|
||||
std::string getToday() {
|
||||
time_t t = time(nullptr);
|
||||
char buf[11];
|
||||
strftime(buf, sizeof(buf), "%Y-%m-%d", localtime(&t));
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
void flushDailyToDB(int code, long long count) {
|
||||
std::string date = getToday();
|
||||
std::string name = keyNames.count(code)
|
||||
? keyNames[code]
|
||||
: "Unknown(" + std::to_string(code) + ")";
|
||||
|
||||
const char* sql =
|
||||
"INSERT INTO daily_counts (date, key_code, key_name, count) VALUES (?, ?, ?, 1)"
|
||||
" ON CONFLICT(date, key_code) DO UPDATE SET count = count + 1;";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr);
|
||||
sqlite3_bind_text(stmt, 1, date.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_bind_int(stmt, 2, code);
|
||||
sqlite3_bind_text(stmt, 3, name.c_str(), -1, SQLITE_TRANSIENT);
|
||||
sqlite3_step(stmt);
|
||||
sqlite3_finalize(stmt);
|
||||
}
|
||||
// ─── Device ───────────────────────────────────────────────────────────────────
|
||||
|
||||
std::string findKeyboard() {
|
||||
DIR* dir = opendir("/dev/input");
|
||||
if (!dir) return "";
|
||||
|
||||
struct dirent* entry;
|
||||
std::string found;
|
||||
|
||||
while ((entry = readdir(dir)) != nullptr) {
|
||||
if (strncmp(entry->d_name, "event", 5) != 0) continue;
|
||||
|
||||
std::string path = "/dev/input/" + std::string(entry->d_name);
|
||||
int tempFd = open(path.c_str(), O_RDONLY);
|
||||
if (tempFd < 0) continue;
|
||||
|
||||
char name[256] = {0};
|
||||
ioctl(tempFd, EVIOCGNAME(sizeof(name)), name);
|
||||
std::string devName(name);
|
||||
for (auto& c : devName) c = tolower(c);
|
||||
|
||||
uint8_t evBits[EV_MAX / 8 + 1] = {0};
|
||||
uint8_t keyBits[KEY_MAX / 8 + 1] = {0};
|
||||
ioctl(tempFd, EVIOCGBIT(0, sizeof(evBits)), evBits);
|
||||
ioctl(tempFd, EVIOCGBIT(EV_KEY, sizeof(keyBits)), keyBits);
|
||||
|
||||
bool hasEvKey = evBits[EV_KEY / 8] & (1 << (EV_KEY % 8));
|
||||
bool hasSpace = keyBits[KEY_SPACE / 8] & (1 << (KEY_SPACE % 8));
|
||||
bool isKeyboard = devName.find("keyboard") != std::string::npos;
|
||||
|
||||
close(tempFd);
|
||||
|
||||
if (hasEvKey && hasSpace && isKeyboard) {
|
||||
found = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return found;
|
||||
}
|
||||
|
||||
// ─── Signal handler ───────────────────────────────────────────────────────────
|
||||
|
||||
void onExit(int) {
|
||||
flushToDB();
|
||||
if (db) sqlite3_close(db);
|
||||
if (fd >= 0) close(fd);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// ─── Main ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
int main() {
|
||||
signal(SIGINT, onExit);
|
||||
signal(SIGTERM, onExit);
|
||||
|
||||
initDB();
|
||||
loadFromDB();
|
||||
|
||||
std::string device = findKeyboard();
|
||||
if (device.empty()) {
|
||||
std::cerr << "No keyboard found. Are you in the 'input' group?\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
fd = open(device.c_str(), O_RDONLY);
|
||||
if (fd < 0) {
|
||||
std::cerr << "Failed to open device.\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct input_event ev;
|
||||
|
||||
while (true) {
|
||||
ssize_t n = read(fd, &ev, sizeof(ev));
|
||||
if (n != sizeof(ev)) break;
|
||||
|
||||
if (ev.type == EV_KEY && ev.value == 1) {
|
||||
keyCounts[ev.code]++;
|
||||
flushToDB();
|
||||
flushDailyToDB(ev.code, keyCounts[ev.code]);
|
||||
}
|
||||
}
|
||||
|
||||
onExit(0);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <algorithm>
|
||||
#include <unordered_map>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <sqlite3.h>
|
||||
#include <pwd.h>
|
||||
#include <unistd.h>
|
||||
#include <ftxui/dom/elements.hpp>
|
||||
#include <ftxui/screen/screen.hpp>
|
||||
#include <ftxui/component/screen_interactive.hpp>
|
||||
#include <ftxui/component/component.hpp>
|
||||
|
||||
using namespace ftxui;
|
||||
|
||||
struct KeyStat {
|
||||
std::string name;
|
||||
long long count;
|
||||
};
|
||||
|
||||
std::string getDBPath() {
|
||||
struct passwd* pw = getpwuid(getuid());
|
||||
return std::string(pw->pw_dir) + "/.keystroke_counts.db";
|
||||
}
|
||||
|
||||
std::vector<KeyStat> lastGoodStats;
|
||||
|
||||
std::vector<KeyStat> loadStats() {
|
||||
sqlite3* db;
|
||||
if (sqlite3_open(getDBPath().c_str(), &db) != SQLITE_OK) return lastGoodStats;
|
||||
|
||||
sqlite3_busy_timeout(db, 100);
|
||||
|
||||
const char* sql = "SELECT key_name, count FROM key_counts ORDER BY count DESC;";
|
||||
sqlite3_stmt* stmt;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
sqlite3_close(db);
|
||||
return lastGoodStats;
|
||||
}
|
||||
|
||||
std::vector<KeyStat> stats;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
std::string name = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
||||
long long count = sqlite3_column_int64(stmt, 1);
|
||||
stats.push_back({name, count});
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
|
||||
if (!stats.empty()) lastGoodStats = stats;
|
||||
return lastGoodStats;
|
||||
}
|
||||
|
||||
std::vector<KeyStat> loadTodayStats() {
|
||||
sqlite3* db;
|
||||
if (sqlite3_open(getDBPath().c_str(), &db) != SQLITE_OK) return {};
|
||||
|
||||
sqlite3_busy_timeout(db, 100);
|
||||
|
||||
time_t t = time(nullptr);
|
||||
char buf[11];
|
||||
strftime(buf, sizeof(buf), "%Y-%m-%d", localtime(&t));
|
||||
std::string today(buf);
|
||||
|
||||
const char* sql =
|
||||
"SELECT key_name, count FROM daily_counts"
|
||||
" WHERE date = ? ORDER BY count DESC;";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
if (sqlite3_prepare_v2(db, sql, -1, &stmt, nullptr) != SQLITE_OK) {
|
||||
sqlite3_close(db);
|
||||
return {};
|
||||
}
|
||||
|
||||
sqlite3_bind_text(stmt, 1, today.c_str(), -1, SQLITE_TRANSIENT);
|
||||
|
||||
std::vector<KeyStat> stats;
|
||||
while (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
std::string name = reinterpret_cast<const char*>(sqlite3_column_text(stmt, 0));
|
||||
long long count = sqlite3_column_int64(stmt, 1);
|
||||
stats.push_back({name, count});
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
sqlite3_close(db);
|
||||
return stats;
|
||||
}
|
||||
|
||||
long long totalCount(const std::vector<KeyStat>& stats) {
|
||||
long long total = 0;
|
||||
for (auto& s : stats) total += s.count;
|
||||
return total;
|
||||
}
|
||||
|
||||
const std::vector<std::vector<std::string>> keyboardLayout = {
|
||||
{"`","1","2","3","4","5","6","7","8","9","0","-","=","Backspace"},
|
||||
{"Tab","Q","W","E","R","T","Y","U","I","O","P","[","]","\\"},
|
||||
{"CapsLock","A","S","D","F","G","H","J","K","L",";","'","Enter"},
|
||||
{"LShift","Z","X","C","V","B","N","M",",",".","/","RShift"},
|
||||
{"LCtrl","Super","LAlt","Space","RAlt","RCtrl"},
|
||||
};
|
||||
|
||||
Color heatColor(long long count, long long maxCount) {
|
||||
if (maxCount == 0 || count == 0) return Color::GrayDark;
|
||||
float ratio = (float)count / (float)maxCount;
|
||||
if (ratio > 0.75f) return Color::Red;
|
||||
if (ratio > 0.50f) return Color::Yellow;
|
||||
if (ratio > 0.25f) return Color::Green;
|
||||
return Color::Blue;
|
||||
}
|
||||
|
||||
Element renderStats(const std::vector<KeyStat>& stats, const std::vector<KeyStat>& todayStats) {
|
||||
if (stats.empty()) {
|
||||
return text("No data yet. Press some keys first.") | color(Color::Red);
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, long long> countMap;
|
||||
for (auto& s : stats) countMap[s.name] = s.count;
|
||||
|
||||
long long maxCount = stats[0].count;
|
||||
long long total = totalCount(stats);
|
||||
|
||||
// ── Heatmap ──────────────────────────────────────────────────────────────
|
||||
std::vector<Element> rows;
|
||||
for (auto& row : keyboardLayout) {
|
||||
std::vector<Element> keys;
|
||||
for (auto& key : row) {
|
||||
long long count = countMap.count(key) ? countMap[key] : 0;
|
||||
Color col = heatColor(count, maxCount);
|
||||
keys.push_back(text(" " + key + " ") | color(col) | border);
|
||||
}
|
||||
rows.push_back(hbox(keys));
|
||||
}
|
||||
|
||||
// ── Lifetime top 10 ──────────────────────────────────────────────────────
|
||||
std::vector<Element> topList;
|
||||
int limit = std::min((int)stats.size(), 10);
|
||||
for (int i = 0; i < limit; i++) {
|
||||
auto& s = stats[i];
|
||||
float pct = 100.0f * s.count / total;
|
||||
topList.push_back(
|
||||
hbox({
|
||||
text(std::to_string(i+1) + ". ") | color(Color::GrayLight),
|
||||
text(s.name) | color(Color::White) | flex,
|
||||
text(std::to_string(s.count)) | color(Color::Yellow),
|
||||
text(" (" + std::to_string((int)pct) + "%)") | color(Color::GrayLight),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// ── Today top 5 ──────────────────────────────────────────────────────────
|
||||
long long todayTotal = totalCount(todayStats);
|
||||
std::vector<Element> todayList;
|
||||
int todayLimit = std::min((int)todayStats.size(), 5);
|
||||
for (int i = 0; i < todayLimit; i++) {
|
||||
auto& s = todayStats[i];
|
||||
float pct = todayTotal > 0 ? 100.0f * s.count / todayTotal : 0;
|
||||
todayList.push_back(
|
||||
hbox({
|
||||
text(std::to_string(i+1) + ". ") | color(Color::GrayLight),
|
||||
text(s.name) | color(Color::White) | flex,
|
||||
text(std::to_string(s.count)) | color(Color::Yellow),
|
||||
text(" (" + std::to_string((int)pct) + "%)") | color(Color::GrayLight),
|
||||
})
|
||||
);
|
||||
}
|
||||
if (todayList.empty()) {
|
||||
todayList.push_back(text("No keys pressed today yet.") | color(Color::GrayDark));
|
||||
}
|
||||
|
||||
// ── Legend ───────────────────────────────────────────────────────────────
|
||||
auto legend = hbox({
|
||||
text(" cold ") | color(Color::GrayDark),
|
||||
text("▓") | color(Color::Blue),
|
||||
text("▓") | color(Color::Green),
|
||||
text("▓") | color(Color::Yellow),
|
||||
text("▓") | color(Color::Red),
|
||||
text(" hot ") | color(Color::GrayLight),
|
||||
});
|
||||
|
||||
return vbox({
|
||||
text(" Keystroke Counter ") | bold | color(Color::Green) | hcenter,
|
||||
separator(),
|
||||
vbox(rows) | hcenter,
|
||||
legend | hcenter,
|
||||
separator(),
|
||||
hbox({
|
||||
vbox({
|
||||
text(" Lifetime Top 10 ") | bold | color(Color::White),
|
||||
separator(),
|
||||
vbox(topList),
|
||||
}) | border | flex,
|
||||
vbox({
|
||||
text(" Today Top 5 ") | bold | color(Color::Green),
|
||||
separator(),
|
||||
vbox(todayList),
|
||||
separator(),
|
||||
text(" Today: " + std::to_string(todayTotal)) | color(Color::Cyan),
|
||||
}) | border | flex,
|
||||
vbox({
|
||||
text(" Total keystrokes ") | bold,
|
||||
text(" " + std::to_string(total)) | color(Color::Cyan) | bold,
|
||||
separator(),
|
||||
text(" q to quit ") | color(Color::GrayDark),
|
||||
}) | border,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
int main() {
|
||||
auto screen = ScreenInteractive::Fullscreen();
|
||||
|
||||
std::atomic<bool> running = true;
|
||||
std::thread refreshThread([&] {
|
||||
while (running) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
screen.PostEvent(Event::Custom);
|
||||
}
|
||||
});
|
||||
|
||||
auto renderer = Renderer([&] {
|
||||
return renderStats(loadStats(), loadTodayStats());
|
||||
});
|
||||
|
||||
auto withQuit = CatchEvent(renderer, [&](Event e) {
|
||||
if (e == Event::Character('q')) {
|
||||
running = false;
|
||||
screen.ExitLoopClosure()();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
screen.Loop(withQuit);
|
||||
refreshThread.join();
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user