Skip to content
Snippets Groups Projects
Verified Commit 0b20e53f authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: initial version

parent a52ef0eb
No related branches found
No related tags found
No related merge requests found
main.go 0 → 100644
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)
}
}
}
{ 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"
'';
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment