For a while I’ve been wanting to imitate Stephen Wolfram’s keylogger system which provides a method for analyzing how much he is working and typing each day.

Prompt:

https://writings.stephenwolfram.com/2012/03/the-personal-analytics-of-my-life/

I would love to write a software to help me keep track of what I type, like Stephen Wolfram does in this article. I only need this software to work on Debian and Ubuntu linux. Can you help me write it in common lisp?

The Final Programs

This initial attempt from ChatGPT works.

;; In SLY: (ql:quickload "bordeaux-threads")

(defpackage :keystroke-logger
  (:use :cl :uiop :bordeaux-threads))


(in-package :keystroke-logger)

(defun log-keystrokes (output-file)
  "Logs keystrokes from xinput to OUTPUT-FILE."
  (let* ((command "xinput test-xi2 --root")
         (process (uiop:launch-program command :output :stream :input :stream)))
    (with-open-file (log-stream output-file :direction :output :if-exists :append :if-does-not-exist :create)
      (loop for line = (read-line (uiop:process-info-output process) nil)
            while line do
        (when (search "KeyPress" line)
          (let ((timestamp (get-universal-time)))
            (format log-stream "~A,~A~%" timestamp line)
            (force-output log-stream)))))))


(defun start-logger ()
  "Starts the keystroke logger in the background."
  (bt:make-thread (lambda () (log-keystrokes "~/Documents/keystrokes.csv")) :name "Keystroke Logger"))

Running and Debugging

;; Start the Logger
(load "~/Documents/keylogger.lisp")
(keystroke-logger:start-logger)

;; View Threads
(bt:all-threads)

;; Destroy a Thread
(bt:destroy-thread (find-if (lambda (x) (string= (bt:thread-name x) "Keystroke Logger")) (bt:all-threads)))

Deepseek Responses

Certainly! Writing a software to track what you type is a great project, and Common Lisp is a powerful language for such tasks. Below, I’ll guide you through creating a simple keylogger in Common Lisp that works on Debian and Ubuntu Linux. This keylogger will log your keystrokes to a file.

Step 1: Install Required Libraries

First, you’ll need to install some libraries to interact with the system and capture keystrokes. We’ll use clx for X11 interaction and trivial-gray-streams for handling streams.

sudo apt-get install libx11-dev

Then, install the necessary Common Lisp libraries using Quicklisp:

(ql:quickload "clx")
(ql:quickload "trivial-gray-streams")

Step 2: Create the Keylogger

Now, let’s write the keylogger in Common Lisp. This script will capture keystrokes and log them to a file.

(defpackage :keylogger
  (:use :cl :xlib :trivial-gray-streams)
  (:export :start-keylogger))

(in-package :keylogger)

(defvar *log-file* "/path/to/your/logfile.txt") ; Change this to your desired log file path

(defun log-keystroke (key)
  (with-open-file (stream *log-file*
                          :direction :output
                          :if-exists :append
                          :if-does-not-exist :create)
    (format stream "~a~%" key)))

(defun start-keylogger ()
  (let* ((display (open-default-display))
         (root (display-default-root-window display))
         (gcontext (create-gcontext :drawable root))
         (keymap (make-array 32 :element-type '(unsigned-byte 8))))
    (unwind-protect
         (loop
           (xlib:query-keymap display keymap)
           (dotimes (i 256)
             (when (logbitp i keymap)
               (let ((key (xlib:keycode->keysym display i 0)))
                 (log-keystroke key))))
           (sleep 0.1)) ; Adjust sleep time as needed
      (close-display display))))

;; Start the keylogger
(start-keylogger)

Step 3: Run the Keylogger

Save the above code to a file, say keylogger.lisp. Then, run it using your Common Lisp implementation (e.g., SBCL):

sbcl --load keylogger.lisp

Important Notes:

  1. Ethical Considerations: Keylogging can be a sensitive activity. Ensure you have the right to log keystrokes on the system where you run this software. Unauthorized keylogging is illegal and unethical.

  2. Security: This is a basic keylogger and does not include any security features. Do not use it on systems where security is a concern.

  3. Customization: You can customize the log-keystroke function to log additional information, such as timestamps or specific key combinations.

  4. Performance: The sleep time in the loop can be adjusted to balance between performance and responsiveness.

This is a simple implementation to get you started. Depending on your needs, you might want to add more features, such as filtering specific keys, logging to a database, or adding a GUI for configuration.

Can you reimplement this using the lower-level ’evdev’ ?

Certainly! Using the evdev interface in Linux allows for more direct access to input events, which is a lower-level approach compared to using X11. This method will work on systems without a graphical environment and is more efficient for keylogging.

Below is a reimplementation of the keylogger using the evdev interface in Common Lisp. This implementation will read input events directly from the Linux input subsystem.


Step 1: Install Required Libraries

You’ll need the evdev library to interact with input devices. On Debian/Ubuntu, install the necessary development headers:

sudo apt-get install libevdev-dev

You’ll also need the cffi library in Common Lisp to interface with the evdev C library:

(ql:quickload "cffi")

Step 2: Create the Keylogger Using evdev

Here’s the Common Lisp code to implement the keylogger using evdev:

(defpackage :evdev-keylogger
  (:use :cl :cffi)
  (:export :start-keylogger))

(in-package :evdev-keylogger)

;; Load the evdev library
(cffi:define-foreign-library libevdev
  (:unix "libevdev.so.2"))

(cffi:use-foreign-library libevdev)

;; Define necessary structures and constants from evdev.h
(cffi:defcstruct input-event
  (time :uint64) ; Timestamp
  (type :uint16) ; Event type
  (code :uint16) ; Event code
  (value :int32)) ; Event value

;; Event types
(defconstant +EV_KEY+ 1) ; Key event

;; Key states
(defconstant +KEY_PRESSED+ 1)
(defconstant +KEY_RELEASED+ 0)

;; Open the input device
(defun open-input-device (device-path)
  (cffi:with-foreign-string (path device-path)
    (let ((fd (cffi:foreign-funcall "open" :string path :int (logior 1 128) :int))) ; O_RDONLY | O_NONBLOCK
      (if (< fd 0)
          (error "Failed to open input device: ~a" device-path)
          fd))))

;; Read input events
(defun read-input-events (fd log-file)
  (cffi:with-foreign-object (event '(:struct input-event))
    (loop
      (let ((bytes-read (cffi:foreign-funcall "read" :int fd :pointer event :size 16 :ssize)))
        (when (> bytes-read 0)
          (let ((event-type (cffi:foreign-slot-value event '(:struct input-event) 'type))
                (event-code (cffi:foreign-slot-value event '(:struct input-event) 'code))
                (event-value (cffi:foreign-slot-value event '(:struct input-event) 'value)))
            (when (and (= event-type +EV_KEY+) (or (= event-value +KEY_PRESSED+) (= event-value +KEY_RELEASED+)))
              (log-keystroke event-code event-value log-file)))))))))

;; Log keystrokes to a file
(defun log-keystroke (key-code key-state log-file)
  (with-open-file (stream log-file
                          :direction :output
                          :if-exists :append
                          :if-does-not-exist :create)
    (format stream "Key: ~a, State: ~a~%" key-code (if (= key-state +KEY_PRESSED+) "Pressed" "Released"))))

;; Start the keylogger
(defun start-keylogger (device-path log-file)
  (let ((fd (open-input-device device-path)))
    (unwind-protect
         (read-input-events fd log-file)
      (cffi:foreign-funcall "close" :int fd :int))))

;; Example usage
;; Replace "/dev/input/eventX" with the correct event device for your keyboard
;; You can find it using `cat /proc/bus/input/devices` or `evtest`
(start-keylogger "/dev/input/eventX" "/path/to/your/logfile.txt")

Step 3: Find the Correct Input Device

To find the correct input device for your keyboard, use the following command:

cat /proc/bus/input/devices

Look for the device that corresponds to your keyboard. It will typically be something like /dev/input/eventX, where X is a number.

Alternatively, you can use the evtest tool to identify the correct device:

sudo apt-get install evtest
evtest

Step 4: Run the Keylogger

Save the above code to a file, say evdev-keylogger.lisp. Then, run it using your Common Lisp implementation (e.g., SBCL):

sbcl --load evdev-keylogger.lisp

Key Points:

  1. Permissions: Reading from /dev/input/eventX requires root privileges. Run the program with sudo or adjust the permissions of the input device.

  2. Ethical Considerations: As with the previous implementation, ensure you have the right to log keystrokes on the system where you run this software.

  3. Customization: You can extend this implementation to include timestamps, filter specific keys, or log to a database.

  4. Performance: This implementation is efficient because it directly reads from the input device without relying on a graphical environment.

This approach is more robust and lower-level than the X11-based implementation, making it suitable for systems without a graphical interface.

ChatGPT Responses

Initial Program

Yes! To track your typing activity in Debian/Ubuntu using Common Lisp, we’ll need to:

  1. Capture Keystrokes – Use a keylogger that works in X11/Wayland.
  2. Process and Store Data – Log keystrokes efficiently in a database or structured file.
  3. Analyze and Visualize – Provide analytics on typing patterns.

For keystroke capture, we can:

  • Use xinput (X11) or libinput (Wayland) via a subprocess.
  • Interface with evdev directly for lower-level access.

For storage and analysis:

  • SQLite (lightweight and easy to query).
  • CSV/JSON (simpler but harder for queries).
  • PostgreSQL (if you want a server-based solution).

Would you like a simple logging script first, or a full-featured application with a UI and analytics?

Let’s create a logging script that runs in the background and saves keystrokes to a CSV file.

We’ll create a Common Lisp script that:

  1. Runs in the background.
  2. Captures keystrokes using xinput (for X11) or evdev (for a lower-level approach).
  3. Logs them in a CSV file with timestamps.

Steps:

  • Step 1: Detect keyboard events using xinput (X11).
  • Step 2: Read output and extract keypresses.
  • Step 3: Write to a CSV file with a timestamp.

Let’s start with a simple implementation using xinput. Here’s a script in Common Lisp:

(defpackage :keystroke-logger
  (:use :cl :uiop))

(in-package :keystroke-logger)

(defun log-keystrokes (output-file)
  "Logs keystrokes from xinput to OUTPUT-FILE."
  (let* ((command "xinput test-xi2 --root")
         (process (uiop:launch-program command :output :stream :input :stream)))
    (with-open-file (log output-file :direction :output :if-exists :append :if-does-not-exist :create)
      (loop for line = (read-line (uiop:process-info-output process) nil)
            while line do
        (when (search "KeyPress" line)
          (let ((timestamp (get-universal-time)))
            (format log "~A,~A~%" timestamp line)
            (force-output log))))))))

(defun start-logger ()
  "Starts the keystroke logger in the background."
  (bt:make-thread (lambda () (log-keystrokes "keystrokes.csv")) :name "Keystroke Logger"))

How It Works:

  • Uses xinput test-xi2 --root to monitor keyboard input.
  • Reads each line, filtering only keypress events.
  • Logs the timestamp and keypress data into keystrokes.csv.
  • Runs in a background thread using bt:make-thread.

Next Steps:

  • Test if xinput test-xi2 --root works on your system (xinput list to check).
  • If using Wayland, we’ll need an alternative (evdev or libinput).

Let me know if you need modifications! 🚀

Reimplementing with ’evdev'

Can you reimplement this program using the lower-level ’evdev’ ?

Yes! Using evdev is a better approach because it works at a lower level and is not limited to X11. It will capture keystrokes even in Wayland or virtual consoles.


Steps to Implement with evdev

  1. Read Keyboard Events – We’ll use /dev/input/eventX (where X is the keyboard device).
  2. Filter Key Presses – Extract keypress events using cffi to interface with evdev.
  3. Log to CSV – Store timestamps and keys in keystrokes.csv.

Prerequisites

You need permission to read /dev/input/event*. Run:

ls /dev/input/event*

Find your keyboard device using:

sudo libinput list-devices | grep -A 10 "Keyboard"

Then, either:

  • Run the program as root (sudo), or
  • Give your user access:
    sudo chmod +r /dev/input/eventX
    

Updated Common Lisp Implementation Using evdev

Here’s the reworked program:

How It Works

  • Uses cffi to interface with evdev.
  • Reads raw keyboard events from /dev/input/eventX.
  • Filters for key press events (event-type = 1 and event-value = 1).
  • Logs timestamp and key code to keystrokes.csv.
  • Runs in a background thread.

Next Steps

  1. Replace X in +evdev-path+ with your keyboard’s event number.
  2. Reload and run:
    (start-logger)
    
  3. Check keystrokes.csv for logged key codes.

Let me know if you need help mapping key codes to characters! 🚀

Running and Debugging with SLY

To test this in a SLY REPL, follow these steps:


1. Load the Script in SLY

Start SLY in Emacs and load your script:

(load "path/to/your/keystroke_logger.lisp")

Alternatively, if you’re editing in a buffer, evaluate individual expressions with C-x C-e (SLY shortcut for evaluating expressions).


2. Manually Call the Logger

You can run the logger function directly in the REPL:

(keystroke-logger:start-logger)

This will start a background thread to log keystrokes.


3. Check If It’s Running

If you want to verify that the logger is running, list active threads:

(bt:all-threads)

You should see a thread named "Keystroke Logger".


4. Verify the Log File

After typing for a while, check the output file:

cat keystrokes.csv

It should contain lines like:

1717349123, KeyPress event, device: 3, code: 38

5. Stop the Logger (If Needed)

If you need to stop the logger, find the thread and terminate it:

(bt:destroy-thread (find-if (lambda (t) (string= (bt:thread-name t) "Keystroke Logger")) (bt:all-threads)))

Troubleshooting

  • If xinput test-xi2 --root fails, check if xinput list lists your keyboard.
  • If using Wayland, this approach may not work. Let me know if you need an evdev-based solution.

Let me know how it works! 🚀

I get an error trying to execute ‘start-logger’ stating “bt does not exist”

You’re right! I forgot to include bordeaux-threads (bt), which is needed for multithreading in Common Lisp. To fix this, do the following:


1. Install Bordeaux-Threads (If Not Installed)

If you don’t have bordeaux-threads already, install it in Quicklisp:

(ql:quickload "bordeaux-threads")

2. Modify Your Script

Add bordeaux-threads to the defpackage declaration:

(defpackage :keystroke-logger
  (:use :cl :uiop :bordeaux-threads))

Then, modify the start-logger function like this:

(defun start-logger ()
  "Starts the keystroke logger in the background."
  (bt:make-thread (lambda () (log-keystrokes "keystrokes.csv"))
                  :name "Keystroke Logger"))

Now, bt:make-thread will work as expected.


3. Reload and Test in SLY

In the SLY REPL:

(ql:quickload "bordeaux-threads")
(load "path/to/keystroke_logger.lisp")
(start-logger)

Next Steps

  • Try typing and check keystrokes.csv for logged keystrokes.
  • If you still get issues, check if xinput test-xi2 --root runs properly in the terminal.

Let me know if you need further debugging! 🚀

It says the function ’log’ is undefined

The issue is that log is a built-in mathematical function in Common Lisp, and we can’t use it as a variable name. To fix this, rename the variable log to something else, like log-stream.

Here’s the corrected version of your function:

(defun log-keystrokes (output-file)
  "Logs keystrokes from xinput to OUTPUT-FILE."
  (let* ((command "xinput test-xi2 --root")
         (process (uiop:launch-program command :output :stream :input :stream)))
    (with-open-file (log-stream output-file :direction :output :if-exists :append :if-does-not-exist :create)
      (loop for line = (read-line (uiop:process-info-output process) nil)
            while line do
        (when (search "KeyPress" line)
          (let ((timestamp (get-universal-time)))
            (format log-stream "~A,~A~%" timestamp line)
            (force-output log-stream)))))))

What Changed?

  • Renamed loglog-stream to avoid conflicts with the built-in log function.
  • Everything else remains the same.

Next Steps

  1. Reload the updated script:
    (load "path/to/keystroke_logger.lisp")
    
  2. Start the logger:
    (start-logger)
    
  3. Check if keystrokes are being logged in keystrokes.csv.

Let me know if you run into any more issues! 🚀