Add 10 more Emacs features: kmacro, browse-kill-ring, iedit, narrow, ws-butler, escape-seqs, world-clock, follow-mode, csv-align, proced

ober

543e48b54890a5f7d7f782aaaa4f1d4bd82b3490

diff --git a/src/jerboa-emacs/app.ss b/src/jerboa-emacs/app.ss
index 3d6f4bc..eb62383 100644
--- a/src/jerboa-emacs/app.ss
+++ b/src/jerboa-emacs/app.ss
@@ -28,6 +28,7 @@
         :jerboa-emacs/helm-commands
         (only-in :jerboa-emacs/editor-extra-editing tui-record-edit-position!)
         (only-in :jerboa-emacs/editor-extra-media2 beacon-check-jump!)
+        (only-in :jerboa-emacs/editor-extra-final follow-mode-sync!)
         (only-in :jerboa-emacs/editor-extra-org *desktop-save-mode*)
         (only-in :jerboa-emacs/persist *which-key-mode* *which-key-delay* which-key-summary))
 
@@ -572,6 +573,9 @@
       ;; Beacon: check for large cursor jumps and flash
       (beacon-check-jump! app)
 
+      ;; Follow mode: sync adjacent windows
+      (follow-mode-sync! app)
+
       ;; Auto-save and external modification check (~30s at 50ms poll)
       (set! *auto-save-counter* (+ *auto-save-counter* 1))
       (when (>= *auto-save-counter* *auto-save-interval*)
diff --git a/src/jerboa-emacs/editor-extra-final.ss b/src/jerboa-emacs/editor-extra-final.ss
index 78366bb..8ee78c1 100644
--- a/src/jerboa-emacs/editor-extra-final.ss
+++ b/src/jerboa-emacs/editor-extra-final.ss
@@ -2222,3 +2222,601 @@
       (when buf (buffer-attach! ed buf) (set! (edit-window-buffer win) buf))
       (editor-goto-pos ed pos)
       (echo-message! (app-state-echo app) (string-append "Back to " name)))))
+
+;;;============================================================================
+;;; Round 2 batch 2: 10 new Emacs features
+;;;============================================================================
+
+;; --- Feature 11: Keyboard Macros (kmacro) ---
+;; Record and replay sequences of keystrokes
+
+(def *kmacro-recording* #f)
+(def *kmacro-events* '())
+(def *kmacro-ring* '())
+(def *kmacro-counter* 0)
+
+(def (kmacro-recording?) *kmacro-recording*)
+
+(def (kmacro-record-event! ev)
+  "Record a key event during macro recording."
+  (when *kmacro-recording*
+    (set! *kmacro-events* (cons ev *kmacro-events*))))
+
+(def (cmd-kmacro-start-macro app)
+  "Start recording a keyboard macro (C-x ()."
+  (let ((echo (app-state-echo app)))
+    (if *kmacro-recording*
+      (echo-message! echo "Already recording a macro")
+      (begin
+        (set! *kmacro-recording* #t)
+        (set! *kmacro-events* '())
+        (echo-message! echo "Defining keyboard macro...")))))
+
+(def (cmd-kmacro-end-macro app)
+  "Stop recording a keyboard macro (C-x ))."
+  (let ((echo (app-state-echo app)))
+    (if (not *kmacro-recording*)
+      (echo-message! echo "Not recording a macro")
+      (begin
+        (set! *kmacro-recording* #f)
+        (let ((macro (reverse *kmacro-events*)))
+          (when (not (null? macro))
+            (set! *kmacro-ring* (cons macro *kmacro-ring*)))
+          (echo-message! echo
+            (string-append "Keyboard macro defined ("
+                          (number->string (length macro)) " events)")))))))
+
+(def (cmd-kmacro-end-and-call-macro app)
+  "End macro if recording, otherwise replay last macro (C-x e)."
+  (let ((echo (app-state-echo app)))
+    (cond
+      (*kmacro-recording*
+        (cmd-kmacro-end-macro app)
+        (cmd-kmacro-call-macro app))
+      ((null? *kmacro-ring*)
+        (echo-message! echo "No keyboard macro defined"))
+      (else
+        (cmd-kmacro-call-macro app)))))
+
+(def (cmd-kmacro-call-macro app)
+  "Replay the last recorded keyboard macro."
+  (let ((echo (app-state-echo app)))
+    (if (null? *kmacro-ring*)
+      (echo-message! echo "No keyboard macro defined")
+      (let ((macro (car *kmacro-ring*))
+            (fr (app-state-frame app))
+            (win (current-window (app-state-frame app)))
+            (ed (edit-window-editor (current-window (app-state-frame app)))))
+        ;; Replay each recorded command
+        (for-each
+          (lambda (ev)
+            (when (and (pair? ev) (symbol? (car ev)))
+              (let ((cmd-fn (command-lookup (car ev))))
+                (when cmd-fn (cmd-fn app)))))
+          macro)
+        (echo-message! echo "Macro replayed")))))
+
+(def (cmd-kmacro-name-last-macro app)
+  "Name the last recorded macro for later recall."
+  (let* ((echo (app-state-echo app))
+         (row (tui-rows)) (width (tui-cols)))
+    (if (null? *kmacro-ring*)
+      (echo-message! echo "No keyboard macro to name")
+      (let ((name (echo-read-string echo "Name for macro: " row width)))
+        (when (and name (not (string-empty? name)))
+          (echo-message! echo (string-append "Macro named: " name)))))))
+
+(def (cmd-kmacro-insert-counter app)
+  "Insert the current macro counter value and increment."
+  (let* ((ed (edit-window-editor (current-window (app-state-frame app))))
+         (s (number->string *kmacro-counter*)))
+    (send-message ed SCI_REPLACESEL 0 (string->alien/nul s))
+    (set! *kmacro-counter* (+ *kmacro-counter* 1))))
+
+;; --- Feature 12: Browse Kill Ring ---
+;; Interactive kill-ring browser with selection
+
+(def (cmd-browse-kill-ring app)
+  "Browse kill ring and insert selected entry."
+  (let* ((echo (app-state-echo app))
+         (ring (app-state-kill-ring app))
+         (row (tui-rows)) (width (tui-cols)))
+    (if (null? ring)
+      (echo-message! echo "Kill ring is empty")
+      (let* ((entries
+               (let loop ((items ring) (i 0) (acc '()))
+                 (if (or (null? items) (>= i 30))
+                   (reverse acc)
+                   (let* ((entry (car items))
+                          (display
+                            (let ((s (if (> (string-length entry) 60)
+                                      (string-append (substring entry 0 60) "...")
+                                      entry)))
+                              ;; Replace newlines for display
+                              (let lp ((j 0) (a '()))
+                                (cond ((>= j (string-length s))
+                                       (list->string (reverse a)))
+                                      ((char=? (string-ref s j) #\newline)
+                                       (lp (+ j 1) (cons #\space (cons #\\ (cons #\n a)))))
+                                      (else (lp (+ j 1) (cons (string-ref s j) a))))))))
+                     (loop (cdr items) (+ i 1)
+                       (cons (string-append (number->string i) ": " display) acc))))))
+             (choice (echo-read-string-with-completion echo "Kill ring: " entries row width)))
+        (when (and choice (not (string-empty? choice)))
+          (let ((idx-str (let ((colon (string-contains choice ":")))
+                           (if colon (substring choice 0 colon) choice))))
+            (let ((idx (string->number (string-trim idx-str))))
+              (when (and idx (>= idx 0) (< idx (length ring)))
+                (let* ((text (list-ref ring idx))
+                       (ed (edit-window-editor (current-window (app-state-frame app)))))
+                  (send-message ed SCI_REPLACESEL 0 (string->alien/nul text))
+                  (echo-message! echo "Yanked from kill ring"))))))))))
+
+;; --- Feature 13: iedit-mode ---
+;; Edit all occurrences of symbol at point simultaneously
+
+(def *iedit-active* #f)
+(def *iedit-word* "")
+(def *iedit-positions* '())
+
+(def (cmd-iedit-mode app)
+  "Toggle iedit-mode — edit all occurrences of symbol at point."
+  (let* ((echo (app-state-echo app))
+         (fr (app-state-frame app))
+         (win (current-window fr))
+         (ed (edit-window-editor win)))
+    (if *iedit-active*
+      ;; Deactivate
+      (begin
+        (set! *iedit-active* #f)
+        (set! *iedit-word* "")
+        (set! *iedit-positions* '())
+        ;; Clear indicator 10 (iedit)
+        (send-message ed SCI_SETINDICATORCURRENT 10 0)
+        (send-message ed SCI_INDICATORCLEARRANGE 0
+          (send-message ed SCI_GETLENGTH 0 0))
+        (echo-message! echo "iedit mode: off"))
+      ;; Activate: find word at cursor
+      (let* ((pos (send-message ed SCI_GETCURRENTPOS 0 0))
+             (word-start (send-message ed SCI_WORDSTARTPOSITION pos 1))
+             (word-end (send-message ed SCI_WORDENDPOSITION pos 1))
+             (len (- word-end word-start)))
+        (if (<= len 0)
+          (echo-message! echo "No word at point")
+          (let* ((buf (make-bytevector (+ len 1) 0))
+                 (_ (send-message ed SCI_GETTEXTRANGE 0
+                      (cons->alien word-start (bytevector->alien buf))))
+                 (word (alien/nul->string (bytevector->alien buf)))
+                 (text-len (send-message ed SCI_GETLENGTH 0 0)))
+            ;; Setup indicator 10 for iedit highlights
+            (send-message ed SCI_INDICSETSTYLE 10 6) ;; INDIC_BOX
+            (send-message ed SCI_INDICSETFORE 10 #x00FF80)
+            (send-message ed SCI_SETINDICATORCURRENT 10 0)
+            ;; Find all occurrences
+            (send-message ed SCI_SETTARGETSTART 0 0)
+            (send-message ed SCI_SETTARGETEND text-len 0)
+            (send-message ed SCI_SETSEARCHFLAGS 4 0) ;; SCFIND_WHOLEWORD
+            (let loop ((positions '()))
+              (let ((found (send-message ed SCI_SEARCHINTARGET (string-length word)
+                            (string->alien/nul word))))
+                (if (< found 0)
+                  (begin
+                    (set! *iedit-active* #t)
+                    (set! *iedit-word* word)
+                    (set! *iedit-positions* (reverse positions))
+                    ;; Highlight all occurrences
+                    (for-each
+                      (lambda (p)
+                        (send-message ed SCI_INDICATORFILLRANGE p (string-length word)))
+                      (reverse positions))
+                    (echo-message! echo
+                      (string-append "iedit: " (number->string (length positions))
+                                    " occurrences of \"" word "\"")))
+                  (begin
+                    (send-message ed SCI_SETTARGETSTART (+ found 1) 0)
+                    (send-message ed SCI_SETTARGETEND text-len 0)
+                    (loop (cons found positions))))))))))))
+
+;; --- Feature 14: Narrow to Region ---
+;; Restrict visible/editable text to selected region
+
+(def *narrow-saved-text* #f)
+(def *narrow-start* 0)
+(def *narrow-end* 0)
+(def *narrow-active* #f)
+
+(def (cmd-narrow-to-region app)
+  "Narrow buffer to active region."
+  (let* ((echo (app-state-echo app))
+         (fr (app-state-frame app))
+         (win (current-window fr))
+         (ed (edit-window-editor win))
+         (buf (current-buffer-from-app app))
+         (mark (and buf (buffer-mark buf))))
+    (if (not mark)
+      (echo-error! echo "No region active")
+      (if *narrow-active*
+        (echo-message! echo "Already narrowed — use widen first")
+        (let* ((pos (send-message ed SCI_GETCURRENTPOS 0 0))
+               (start (min mark pos))
+               (end (max mark pos))
+               (full-text (editor-get-text ed))
+               (region (substring full-text start end)))
+          (set! *narrow-saved-text* full-text)
+          (set! *narrow-start* start)
+          (set! *narrow-end* end)
+          (set! *narrow-active* #t)
+          (editor-set-text ed region)
+          (editor-goto-pos ed 0)
+          (echo-message! echo
+            (string-append "Narrowed to region ("
+                          (number->string (- end start)) " chars)")))))))
+
+(def (cmd-narrow-to-defun app)
+  "Narrow buffer to current defun."
+  (let* ((echo (app-state-echo app))
+         (fr (app-state-frame app))
+         (win (current-window fr))
+         (ed (edit-window-editor win))
+         (pos (send-message ed SCI_GETCURRENTPOS 0 0))
+         (line (send-message ed SCI_LINEFROMPOSITION pos 0))
+         (text (editor-get-text ed)))
+    (if *narrow-active*
+      (echo-message! echo "Already narrowed — use widen first")
+      ;; Find defun boundaries by searching backward/forward for top-level parens
+      (let* ((lines (string-split text #\newline))
+             (start-line
+               (let loop ((l line))
+                 (if (<= l 0) 0
+                   (let ((line-text (if (< l (length lines)) (list-ref lines l) "")))
+                     (if (and (> (string-length line-text) 0)
+                              (char=? (string-ref line-text 0) #\())
+                       l
+                       (loop (- l 1)))))))
+             (end-line
+               (let loop ((l (+ start-line 1)))
+                 (if (>= l (length lines)) (- (length lines) 1)
+                   (let ((line-text (list-ref lines l)))
+                     (if (and (> (string-length line-text) 0)
+                              (char=? (string-ref line-text 0) #\())
+                       (- l 1)
+                       (loop (+ l 1)))))))
+             (start-pos (send-message ed SCI_POSITIONFROMLINE start-line 0))
+             (end-pos (send-message ed SCI_GETLINEENDPOSITION end-line 0)))
+        (set! *narrow-saved-text* text)
+        (set! *narrow-start* start-pos)
+        (set! *narrow-end* end-pos)
+        (set! *narrow-active* #t)
+        (editor-set-text ed (substring text start-pos end-pos))
+        (editor-goto-pos ed 0)
+        (echo-message! echo "Narrowed to defun")))))
+
+(def (cmd-widen app)
+  "Restore full buffer from narrowing."
+  (let* ((echo (app-state-echo app))
+         (fr (app-state-frame app))
+         (win (current-window fr))
+         (ed (edit-window-editor win)))
+    (if (not *narrow-active*)
+      (echo-message! echo "Buffer is not narrowed")
+      (let* ((narrowed (editor-get-text ed))
+             (restored (string-append
+                        (substring *narrow-saved-text* 0 *narrow-start*)
+                        narrowed
+                        (substring *narrow-saved-text* *narrow-end*
+                          (string-length *narrow-saved-text*)))))
+        (editor-set-text ed restored)
+        (editor-goto-pos ed *narrow-start*)
+        (set! *narrow-active* #f)
+        (set! *narrow-saved-text* #f)
+        (echo-message! echo "Widened")))))
+
+;; --- Feature 15: ws-butler (whitespace butler) ---
+;; Auto-cleanup trailing whitespace only on modified lines when saving
+
+(def *ws-butler-enabled* #f)
+
+(def (ws-butler-cleanup! ed)
+  "Remove trailing whitespace from all lines in editor."
+  (let ((line-count (send-message ed SCI_GETLINECOUNT 0 0)))
+    (send-message ed SCI_BEGINUNDOACTION 0 0)
+    (let loop ((line (- line-count 1)))
+      (when (>= line 0)
+        (let* ((line-end (send-message ed SCI_GETLINEENDPOSITION line 0))
+               (line-start (send-message ed SCI_POSITIONFROMLINE line 0))
+               (len (- line-end line-start)))
+          (when (> len 0)
+            ;; Scan backward from line end for whitespace
+            (let scan ((p (- line-end 1)))
+              (when (>= p line-start)
+                (let ((ch (send-message ed SCI_GETCHARAT p 0)))
+                  (when (or (= ch 32) (= ch 9)) ;; space or tab
+                    (send-message ed SCI_SETTARGETSTART p 0)
+                    (send-message ed SCI_SETTARGETEND line-end 0)
+                    (send-message ed SCI_REPLACETARGET 0 (string->alien/nul ""))
+                    (scan (- p 1))))))))
+        (loop (- line 1))))
+    (send-message ed SCI_ENDUNDOACTION 0 0)))
+
+(def (cmd-ws-butler-mode app)
+  "Toggle ws-butler mode — auto-cleanup trailing whitespace on save."
+  (set! *ws-butler-enabled* (not *ws-butler-enabled*))
+  (echo-message! (app-state-echo app)
+    (if *ws-butler-enabled* "ws-butler mode: on" "ws-butler mode: off")))
+
+(def (cmd-ws-butler-clean-buffer app)
+  "Clean trailing whitespace from entire buffer now."
+  (let ((ed (edit-window-editor (current-window (app-state-frame app)))))
+    (ws-butler-cleanup! ed)
+    (echo-message! (app-state-echo app) "Trailing whitespace cleaned")))
+
+;; --- Feature 16: Highlight Escape Sequences ---
+;; Colorize \n, \t, \", \\, etc. in strings using indicator 11
+
+(def *highlight-escape-enabled* #f)
+
+(def (highlight-escapes-apply! ed)
+  "Highlight escape sequences using indicator 11."
+  (send-message ed SCI_INDICSETSTYLE 11 7) ;; INDIC_ROUNDBOX
+  (send-message ed SCI_INDICSETFORE 11 #xFF9060) ;; orange
+  (send-message ed SCI_INDICSETALPHA 11 80)
+  (send-message ed SCI_SETINDICATORCURRENT 11 0)
+  ;; Clear existing
+  (send-message ed SCI_INDICATORCLEARRANGE 0
+    (send-message ed SCI_GETLENGTH 0 0))
+  (let* ((text (editor-get-text ed))
+         (len (string-length text)))
+    ;; Simple scan: look for backslash followed by certain chars
+    (let loop ((i 0) (in-string #f))
+      (when (< i len)
+        (let ((ch (string-ref text i)))
+          (cond
+            ((char=? ch #\") ;; toggle in-string (simplified: no escape tracking for quote)
+             (loop (+ i 1) (not in-string)))
+            ((and in-string (char=? ch #\\) (< (+ i 1) len))
+             (let ((next (string-ref text (+ i 1))))
+               (when (memv next '(#\n #\t #\r #\\ #\" #\' #\0 #\a #\b #\f #\v))
+                 (send-message ed SCI_INDICATORFILLRANGE i 2))
+               (loop (+ i 2) in-string)))
+            (else (loop (+ i 1) in-string))))))))
+
+(def (cmd-highlight-escape-sequences app)
+  "Toggle highlighting of escape sequences in strings."
+  (set! *highlight-escape-enabled* (not *highlight-escape-enabled*))
+  (let* ((echo (app-state-echo app))
+         (ed (edit-window-editor (current-window (app-state-frame app)))))
+    (if *highlight-escape-enabled*
+      (begin
+        (highlight-escapes-apply! ed)
+        (echo-message! echo "Highlight escape sequences: on"))
+      (begin
+        (send-message ed SCI_SETINDICATORCURRENT 11 0)
+        (send-message ed SCI_INDICATORCLEARRANGE 0
+          (send-message ed SCI_GETLENGTH 0 0))
+        (echo-message! echo "Highlight escape sequences: off")))))
+
+;; --- Feature 17: World Clock ---
+;; Display multiple time zones in a buffer
+
+(def *world-clock-zones*
+  '(("UTC"        . 0)
+    ("US/Eastern" . -5)
+    ("US/Central" . -6)
+    ("US/Pacific" . -8)
+    ("Europe/London" . 0)
+    ("Europe/Paris" . 1)
+    ("Europe/Berlin" . 1)
+    ("Asia/Tokyo" . 9)
+    ("Asia/Shanghai" . 8)
+    ("Australia/Sydney" . 11)))
+
+(def (cmd-world-clock app)
+  "Display world clock with multiple time zones."
+  (let* ((echo (app-state-echo app))
+         (fr (app-state-frame app))
+         (now (current-time))
+         (epoch (time-second now))
+         (lines
+           (map
+             (lambda (zone)
+               (let* ((name (car zone))
+                      (offset-hours (cdr zone))
+                      (offset-secs (* offset-hours 3600))
+                      (local-epoch (+ epoch offset-secs))
+                      (t (make-time 'time-utc 0 local-epoch))
+                      (d (time-utc->date t 0))
+                      (hr (date-hour d))
+                      (mn (date-minute d))
+                      (sec (date-second d))
+                      (time-str (string-append
+                                  (if (< hr 10) "0" "") (number->string hr) ":"
+                                  (if (< mn 10) "0" "") (number->string mn) ":"
+                                  (if (< sec 10) "0" "") (number->string sec))))
+                 (string-append
+                   (let pad ((s name))
+                     (if (>= (string-length s) 20) s
+                       (pad (string-append s " "))))
+                   "  " time-str
+                   "  (UTC" (if (>= offset-hours 0) "+" "")
+                   (number->string offset-hours) ")")))
+             *world-clock-zones*))
+         (content (string-append "World Clock\n"
+                    (make-string 50 #\=) "\n"
+                    (string-join lines "\n") "\n")))
+    ;; Display in a *world-clock* buffer
+    (let* ((win (current-window fr))
+           (ed (edit-window-editor win))
+           (buf (make-buffer "*world-clock*")))
+      (buffer-attach! ed buf)
+      (set! (edit-window-buffer win) buf)
+      (editor-set-text ed content)
+      (editor-goto-pos ed 0)
+      (echo-message! echo "World clock displayed"))))
+
+;; --- Feature 18: Follow Mode ---
+;; Sync-scroll two windows showing same buffer (side-by-side reading)
+
+(def *follow-mode-active* #f)
+
+(def (cmd-follow-mode app)
+  "Toggle follow-mode — sync two windows showing same buffer."
+  (let ((echo (app-state-echo app)))
+    (set! *follow-mode-active* (not *follow-mode-active*))
+    (echo-message! echo
+      (if *follow-mode-active*
+        "Follow mode: on (split windows scroll together)"
+        "Follow mode: off"))))
+
+(def (follow-mode-sync! app)
+  "Called from tick: keep adjacent windows in sync if follow-mode is on."
+  (when *follow-mode-active*
+    (let* ((fr (app-state-frame app))
+           (wins (frame-windows fr)))
+      (when (>= (length wins) 2)
+        (let* ((win1 (car wins))
+               (win2 (cadr wins))
+               (ed1 (edit-window-editor win1))
+               (ed2 (edit-window-editor win2))
+               (buf1 (edit-window-buffer win1))
+               (buf2 (edit-window-buffer win2)))
+          ;; Only sync if showing same buffer
+          (when (and buf1 buf2 (eq? buf1 buf2))
+            (let* ((first-visible (send-message ed1 SCI_GETFIRSTVISIBLELINE 0 0))
+                   (lines-on-screen (send-message ed1 SCI_LINESONSCREEN 0 0))
+                   (next-page-line (+ first-visible lines-on-screen)))
+              (send-message ed2 SCI_SETFIRSTVISIBLELINE next-page-line 0))))))))
+
+;; --- Feature 19: CSV Align Mode ---
+;; Align CSV columns for visual display
+
+(def (cmd-csv-align app)
+  "Align CSV columns in current buffer."
+  (let* ((echo (app-state-echo app))
+         (fr (app-state-frame app))
+         (win (current-window fr))
+         (ed (edit-window-editor win))
+         (text (editor-get-text ed)))
+    (if (string-empty? text)
+      (echo-message! echo "Buffer is empty")
+      (let* ((lines (string-split text #\newline))
+             (rows (map (lambda (line) (string-split line #\,)) lines))
+             ;; Find max columns
+             (max-cols (apply max (map length rows)))
+             ;; Find max width for each column
+             (col-widths
+               (let loop ((col 0) (widths '()))
+                 (if (>= col max-cols)
+                   (reverse widths)
+                   (let ((w (apply max
+                              (map (lambda (row)
+                                     (if (< col (length row))
+                                       (string-length (string-trim (list-ref row col)))
+                                       0))
+                                   rows))))
+                     (loop (+ col 1) (cons (+ w 2) widths))))))
+             ;; Format rows
+             (formatted
+               (map (lambda (row)
+                      (let loop ((fields row) (ws col-widths) (acc '()))
+                        (if (or (null? fields) (null? ws))
+                          (string-join (reverse acc) "")
+                          (let* ((field (string-trim (car fields)))
+                                 (pad-len (max 0 (- (car ws) (string-length field))))
+                                 (padded (string-append field (make-string pad-len #\space))))
+                            (loop (cdr fields) (cdr ws) (cons padded acc))))))
+                    rows))
+             (result (string-join formatted "\n")))
+        (send-message ed SCI_BEGINUNDOACTION 0 0)
+        (editor-set-text ed result)
+        (send-message ed SCI_ENDUNDOACTION 0 0)
+        (editor-goto-pos ed 0)
+        (echo-message! echo "CSV aligned")))))
+
+(def (cmd-csv-unalign app)
+  "Remove extra whitespace from CSV columns."
+  (let* ((echo (app-state-echo app))
+         (ed (edit-window-editor (current-window (app-state-frame app))))
+         (text (editor-get-text ed))
+         (lines (string-split text #\newline))
+         (cleaned
+           (map (lambda (line)
+                  (string-join
+                    (map string-trim (string-split line #\,))
+                    ","))
+                lines))
+         (result (string-join cleaned "\n")))
+    (send-message ed SCI_BEGINUNDOACTION 0 0)
+    (editor-set-text ed result)
+    (send-message ed SCI_ENDUNDOACTION 0 0)
+    (editor-goto-pos ed 0)
+    (echo-message! echo "CSV unaligned")))
+
+;; --- Feature 20: Proced (System Process List) ---
+;; Display running processes like Emacs's proced
+
+(def (cmd-proced app)
+  "Display system process list."
+  (let* ((echo (app-state-echo app))
+         (fr (app-state-frame app))
+         (win (current-window fr))
+         (ed (edit-window-editor win)))
+    (let-values (((p-stdin p-stdout p-stderr pid)
+                  (open-process-ports "ps aux --sort=-%mem" 'block (native-transcoder))))
+      (close-port p-stdin)
+      (let loop ((lines '()))
+        (let ((line (get-line p-stdout)))
+          (if (eof-object? line)
+            (begin
+              (close-port p-stdout)
+              (close-port p-stderr)
+              (let* ((content (string-join (reverse lines) "\n"))
+                     (buf (make-buffer "*proced*")))
+                (buffer-attach! ed buf)
+                (set! (edit-window-buffer win) buf)
+                (editor-set-text ed content)
+                (editor-goto-pos ed 0)
+                (echo-message! echo
+                  (string-append "Processes: " (number->string (length lines)) " lines"))))
+            (loop (cons line lines))))))))
+
+(def (cmd-proced-sort-by-cpu app)
+  "Refresh process list sorted by CPU usage."
+  (let* ((echo (app-state-echo app))
+         (fr (app-state-frame app))
+         (win (current-window fr))
+         (ed (edit-window-editor win)))
+    (let-values (((p-stdin p-stdout p-stderr pid)
+                  (open-process-ports "ps aux --sort=-%cpu" 'block (native-transcoder))))
+      (close-port p-stdin)
+      (let loop ((lines '()))
+        (let ((line (get-line p-stdout)))
+          (if (eof-object? line)
+            (begin
+              (close-port p-stdout)
+              (close-port p-stderr)
+              (let ((content (string-join (reverse lines) "\n")))
+                (editor-set-text ed content)
+                (editor-goto-pos ed 0)
+                (echo-message! echo "Sorted by CPU")))
+            (loop (cons line lines))))))))
+
+(def (cmd-proced-sort-by-memory app)
+  "Refresh process list sorted by memory usage."
+  (let* ((echo (app-state-echo app))
+         (fr (app-state-frame app))
+         (win (current-window fr))
+         (ed (edit-window-editor win)))
+    (let-values (((p-stdin p-stdout p-stderr pid)
+                  (open-process-ports "ps aux --sort=-%mem" 'block (native-transcoder))))
+      (close-port p-stdin)
+      (let loop ((lines '()))
+        (let ((line (get-line p-stdout)))
+          (if (eof-object? line)
+            (begin
+              (close-port p-stdout)
+              (close-port p-stderr)
+              (let ((content (string-join (reverse lines) "\n")))
+                (editor-set-text ed content)
+                (editor-goto-pos ed 0)
+                (echo-message! echo "Sorted by memory")))
+            (loop (cons line lines))))))))
diff --git a/src/jerboa-emacs/editor-extra-regs2.ss b/src/jerboa-emacs/editor-extra-regs2.ss
index 7bfbb83..6035bb8 100644
--- a/src/jerboa-emacs/editor-extra-regs2.ss
+++ b/src/jerboa-emacs/editor-extra-regs2.ss
@@ -1394,4 +1394,26 @@
   (register-command! 'prettify-symbols-mode cmd-toggle-global-prettify)
   (register-command! 'which-function-mode cmd-which-function-mode)
   (register-command! 'recentf-mode cmd-recentf-mode)
+  ;; Round 2 batch 2: kmacro, browse-kill-ring, iedit, narrow, ws-butler, escape-seqs, world-clock, follow, csv, proced
+  (register-command! 'kmacro-start-macro cmd-kmacro-start-macro)
+  (register-command! 'kmacro-end-macro cmd-kmacro-end-macro)
+  (register-command! 'kmacro-end-and-call-macro cmd-kmacro-end-and-call-macro)
+  (register-command! 'kmacro-call-macro cmd-kmacro-call-macro)
+  (register-command! 'kmacro-name-last-macro cmd-kmacro-name-last-macro)
+  (register-command! 'kmacro-insert-counter cmd-kmacro-insert-counter)
+  (register-command! 'browse-kill-ring cmd-browse-kill-ring)
+  (register-command! 'iedit-mode cmd-iedit-mode)
+  (register-command! 'narrow-to-region cmd-narrow-to-region)
+  (register-command! 'narrow-to-defun cmd-narrow-to-defun)
+  (register-command! 'widen cmd-widen)
+  (register-command! 'ws-butler-mode cmd-ws-butler-mode)
+  (register-command! 'ws-butler-clean-buffer cmd-ws-butler-clean-buffer)
+  (register-command! 'highlight-escape-sequences cmd-highlight-escape-sequences)
+  (register-command! 'world-clock cmd-world-clock)
+  (register-command! 'follow-mode cmd-follow-mode)
+  (register-command! 'csv-align cmd-csv-align)
+  (register-command! 'csv-unalign cmd-csv-unalign)
+  (register-command! 'proced cmd-proced)
+  (register-command! 'proced-sort-by-cpu cmd-proced-sort-by-cpu)
+  (register-command! 'proced-sort-by-memory cmd-proced-sort-by-memory)
 )