From 0b20e53fef64d7b3b8069f66e54858c933573176 Mon Sep 17 00:00:00 2001 From: Volker Schukai <volker.schukai@schukai.com> Date: Mon, 6 Jan 2025 17:59:09 +0100 Subject: [PATCH] feat: initial version --- main.go | 261 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ shell.nix | 21 +++++ 2 files changed, 282 insertions(+) create mode 100644 main.go create mode 100644 shell.nix diff --git a/main.go b/main.go new file mode 100644 index 0000000..c4e5f5b --- /dev/null +++ b/main.go @@ -0,0 +1,261 @@ +package main + +/* +#cgo LDFLAGS: -lX11 +#include <X11/Xlib.h> +#include <stdlib.h> +*/ +import "C" + +import ( + "fmt" + "math" + "os" + "strings" + "time" + "unsafe" +) + +var ( + // --- X / Windowing --- + disp *C.Display + root C.Window + netActiveWindow C.Atom + netWmName C.Atom + + // Aktuell aktives Fenster + currentActiveWindow C.Window + + // --- Zähler für Eingaben --- + keyPressCount int // Anzahl Tastendrücke + mouseDistance float64 // Summe der zurückgelegten Maus-Distanz (in Pixel) + lastX, lastY int // letztes Maus-Event, um Distanz zu berechnen + haveLastPos bool // ob wir schon einmal eine Mausposition haben + + // --- CSV-Log --- + csvFile *os.File +) + +// getActiveWindow liest das aktive Fenster aus _NET_ACTIVE_WINDOW +func getActiveWindow(disp *C.Display, root C.Window, atom C.Atom) C.Window { + var actualType C.Atom + var actualFormat C.int + var nItems, bytesAfter C.ulong + var prop *C.uchar + + status := C.XGetWindowProperty( + disp, + root, + atom, + 0, 1, + C.False, + C.AnyPropertyType, + &actualType, &actualFormat, &nItems, &bytesAfter, &prop, + ) + if status != C.Success || prop == nil { + return 0 + } + defer C.XFree(unsafe.Pointer(prop)) + + return *(*C.Window)(unsafe.Pointer(prop)) +} + +// getNetWmName liest den UTF-8 Fenstertitel aus _NET_WM_NAME +func getNetWmName(disp *C.Display, window C.Window, atom C.Atom) string { + if window == 0 { + return "(no window)" + } + + var actualType C.Atom + var actualFormat C.int + var nItems, bytesAfter C.ulong + var prop *C.uchar + + status := C.XGetWindowProperty( + disp, + window, + atom, + 0, 1<<20, // Bis zu ~1 MB Zeichen + C.False, + C.AnyPropertyType, + &actualType, &actualFormat, &nItems, &bytesAfter, &prop, + ) + if status != C.Success || prop == nil { + return "(unknown)" + } + defer C.XFree(unsafe.Pointer(prop)) + + return C.GoString((*C.char)(unsafe.Pointer(prop))) +} + +// parseTabTitle extrahiert den Teil vor " - " als simplen Tab-Namen +func parseTabTitle(fullTitle string) string { + parts := strings.SplitN(fullTitle, " - ", 2) + if len(parts) == 2 { + return parts[0] + } + return fullTitle +} + +// resetInputStats setzt Zähler für Tastatur/Maus zurück +func resetInputStats() { + keyPressCount = 0 + mouseDistance = 0 + haveLastPos = false +} + +// logToCSV schreibt einen Eintrag in die CSV-Datei. +// Anschließend werden Key/Mauszähler zurückgesetzt (d. h. gezählt wird immer +// "seit dem letzten Eintrag"). +func logToCSV(win C.Window, fullTitle, tabTitle string) { + timestamp := time.Now().Format("2006-01-02 15:04:05") + + // Zeile bauen. Ggf. CSV escaping anpassen, hier minimal. + // Spalten: Timestamp,WindowHex,WindowTitle,Tab,KeyPresses,MouseDistance + line := fmt.Sprintf( + "%s,0x%x,\"%s\",\"%s\",%d,%.2f\n", + timestamp, win, fullTitle, tabTitle, + keyPressCount, mouseDistance, + ) + + _, err := csvFile.WriteString(line) + if err != nil { + fmt.Printf("Fehler beim Schreiben in CSV: %v\n", err) + } + // zur Sicherheit direkt flushen + csvFile.Sync() + + // Danach Zähler zurücksetzen + resetInputStats() +} + +func main() { + // X Display öffnen + disp = C.XOpenDisplay(nil) + if disp == nil { + panic("Cannot open display") + } + defer C.XCloseDisplay(disp) + + // Root-Fenster + root = C.XDefaultRootWindow(disp) + + // CSV-Datei öffnen (oder erstellen) zum Anhängen + var err error + csvFile, err = os.OpenFile("data.csv", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + panic(err) + } + defer csvFile.Close() + + // Header nur schreiben, wenn Datei leer war + info, _ := csvFile.Stat() + if info.Size() == 0 { + csvFile.WriteString("Timestamp,WindowHex,WindowTitle,Tab,KeyPresses,MouseDistance\n") + } + + // Atoms abfragen + netActiveWindow = C.XInternAtom(disp, C.CString("_NET_ACTIVE_WINDOW"), C.True) + netWmName = C.XInternAtom(disp, C.CString("_NET_WM_NAME"), C.True) + if netActiveWindow == 0 || netWmName == 0 { + panic("Could not retrieve EWMH atoms") + } + + // Auf dem Root-Fenster: + // - PropertyChangeMask => _NET_ACTIVE_WINDOW ändert sich + // - KeyPressMask, PointerMotionMask => Tastendrücke / Mausbewegung + // (ACHTUNG: KeyPressMask / PointerMotionMask am Root-Fenster + // funktioniert nur, wenn der WM die Events "durchlässt" + // oder du ein globales Keyboard/Mausgrab machst. + // Je nach WM/Umgebung klappt das ggf. nicht perfekt.) + + eventMask := C.long(C.PropertyChangeMask | C.KeyPressMask | C.PointerMotionMask) + C.XSelectInput(disp, root, eventMask) + + // eventMask := (C.PropertyChangeMask | + // C.KeyPressMask | + // C.PointerMotionMask) + // C.XSelectInput(disp, root, eventMask) + + // aktuelles aktives Fenster ermitteln + currentActiveWindow = getActiveWindow(disp, root, netActiveWindow) + + // Erstes Logging, falls wir schon ein aktives Fenster haben + if currentActiveWindow != 0 { + // Für Titeländerungen in diesem Fenster: Auch hier Eventmask setzen + // (PropertyChangeMask => _NET_WM_NAME). + C.XSelectInput(disp, currentActiveWindow, C.PropertyChangeMask) + + title := getNetWmName(disp, currentActiveWindow, netWmName) + tab := parseTabTitle(title) + fmt.Printf("Initially active: 0x%x - %s\n", currentActiveWindow, title) + + // Log in CSV + Reset + logToCSV(currentActiveWindow, title, tab) + } + + fmt.Println("Starte Event-Loop (Fenster/Tab + KeyPress + Mausbewegung) ...") + + // Hauptschleife + var ev C.XEvent + for { + C.XNextEvent(disp, &ev) + evType := (*C.XAnyEvent)(unsafe.Pointer(&ev))._type + + switch evType { + // 1) TITLE- oder ACTIVE-Window-Änderungen + case C.PropertyNotify: + xprop := (*C.XPropertyEvent)(unsafe.Pointer(&ev)) + + // a) _NET_ACTIVE_WINDOW am Root: Neues Fenster aktiv + if xprop.window == root && xprop.atom == netActiveWindow { + newActive := getActiveWindow(disp, root, netActiveWindow) + if newActive != currentActiveWindow { + currentActiveWindow = newActive + fmt.Printf("\nActive window changed: 0x%x\n", currentActiveWindow) + + // Jetzt am neuen aktiven Fenster: PropertyChangeMask (Titeländerungen) + C.XSelectInput(disp, currentActiveWindow, C.PropertyChangeMask) + + title := getNetWmName(disp, currentActiveWindow, netWmName) + tab := parseTabTitle(title) + fmt.Printf("Title: %s\n=> Tab: %s\n", title, tab) + + logToCSV(currentActiveWindow, title, tab) + } + + // b) _NET_WM_NAME am aktuell aktiven Fenster => Titel/Tab-Wechsel + } else if xprop.window == currentActiveWindow && xprop.atom == netWmName { + title := getNetWmName(disp, currentActiveWindow, netWmName) + tab := parseTabTitle(title) + fmt.Printf("\nTitle changed on active window (0x%x): %s\n=> Tab: %s\n", + currentActiveWindow, title, tab) + + logToCSV(currentActiveWindow, title, tab) + } + + // 2) Tastendrücke + case C.KeyPress: + keyPressCount++ + + // 3) Mausbewegung + case C.MotionNotify: + motion := (*C.XMotionEvent)(unsafe.Pointer(&ev)) + x := int(motion.x_root) + y := int(motion.y_root) + if haveLastPos { + dx := float64(x - lastX) + dy := float64(y - lastY) + mouseDistance += math.Sqrt(dx*dx + dy*dy) + } else { + haveLastPos = true + } + lastX = x + lastY = y + + default: + // Andere Events ignorieren + // fmt.Printf("Unhandled event type: %d\n", evType) + } + } +} diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..90817b5 --- /dev/null +++ b/shell.nix @@ -0,0 +1,21 @@ +{ pkgs ? import <nixpkgs> {} }: + +pkgs.mkShell { + buildInputs = [ + pkgs.xorg.libX11 + pkgs.go + pkgs.libuiohook + pkgs.gcc + ]; + + shellHook = '' + export CGO_ENABLED=1 + export CGO_CFLAGS="-I${pkgs.xorg.libX11.dev}/include" + export CGO_LDFLAGS="-L${pkgs.xorg.libX11.out}/lib" + + export CGO_CFLAGS="''${CGO_CFLAGS} -I${pkgs.libuiohook}/include" + export CGO_LDFLAGS="''${CGO_LDFLAGS} -L${pkgs.libuiohook}/lib" + + ''; +} + -- GitLab