commit fa9a71541b163d6ec8902cd8d7a678f7cfe03ddd Author: Wander_Lust Date: Sun Mar 15 23:10:22 2026 +0530 first commit - mai san diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c6c37e1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +build/ +*.db +*.o +*.out +.cache/ +compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..30708f9 --- /dev/null +++ b/CMakeLists.txt @@ -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 +) diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f665025 --- /dev/null +++ b/Makefile @@ -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 diff --git a/keystats.service b/keystats.service new file mode 100644 index 0000000..a8d3acc --- /dev/null +++ b/keystats.service @@ -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 diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..91b7332 --- /dev/null +++ b/main.cpp @@ -0,0 +1,240 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// ─── Key name map ───────────────────────────────────────────────────────────── + +std::unordered_map 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 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; +} diff --git a/stats.cpp b/stats.cpp new file mode 100644 index 0000000..c6bb861 --- /dev/null +++ b/stats.cpp @@ -0,0 +1,241 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 lastGoodStats; + +std::vector 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 stats; + while (sqlite3_step(stmt) == SQLITE_ROW) { + std::string name = reinterpret_cast(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 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 stats; + while (sqlite3_step(stmt) == SQLITE_ROW) { + std::string name = reinterpret_cast(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& stats) { + long long total = 0; + for (auto& s : stats) total += s.count; + return total; +} + +const std::vector> 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& stats, const std::vector& todayStats) { + if (stats.empty()) { + return text("No data yet. Press some keys first.") | color(Color::Red); + } + + std::unordered_map countMap; + for (auto& s : stats) countMap[s.name] = s.count; + + long long maxCount = stats[0].count; + long long total = totalCount(stats); + + // ── Heatmap ────────────────────────────────────────────────────────────── + std::vector rows; + for (auto& row : keyboardLayout) { + std::vector 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 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 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 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; +}