▍ humdrum codex / glint v1.0.2
license AGPL-3.0

feat: Ctrl+U / Ctrl+K kill to line start / end

cfcd5282e4cab95208e6e04ed95863c16ffe6f3e
humdrum <me@humdrum.me> · 2026-06-28 17:32

parent b029710a

feat: Ctrl+U / Ctrl+K kill to line start / end

Ctrl+U deletes from the start of the line to the cursor (matches the macOS
Cmd+Delete behavior — map Cmd+Delete→Ctrl+U in the terminal); Ctrl+K deletes
to the end. Both stay on the logical line, set dirty, and refresh goal+scroll.

3 files changed

README.md +1 −0
@@ -38,6 +38,7 @@
 | Key | Action |
 | --- | --- |
 | type / arrows / `Enter` / `Backspace` / `Del` | edit and move (Up/Down move by visual line) |
+| `Ctrl+U` / `Ctrl+K` | delete to start / end of line (map `Cmd+Delete`→`Ctrl+U` in your terminal) |
 | `Ctrl+S` | save (an unnamed buffer prompts for a name → inbox) |
 | `Ctrl+P` | toggle the Glamour read preview |
 | `Ctrl+F` | fuzzy file picker (with live preview) |
internal/editor/editor.go +29 −0
@@ -71,6 +71,31 @@
 func (e *Editor) curLine() []rune   { return []rune(e.Lines[e.Cursor.Row]) }
 func (e *Editor) setLine(rs []rune) { e.Lines[e.Cursor.Row] = string(rs) }
 
+// KillToLineStart deletes from the start of the line to the cursor and parks the
+// cursor at column 0 (Ctrl+U; the macOS Cmd+Delete behavior).
+func (e *Editor) KillToLineStart() {
+	if e.Cursor.Col == 0 {
+		return
+	}
+	e.setLine(e.curLine()[e.Cursor.Col:])
+	e.Cursor.Col = 0
+	e.Dirty = true
+	e.setGoal()
+	e.followCursor()
+}
+
+// KillToLineEnd deletes from the cursor to the end of the line (Ctrl+K).
+func (e *Editor) KillToLineEnd() {
+	rs := e.curLine()
+	if e.Cursor.Col >= len(rs) {
+		return
+	}
+	e.setLine(rs[:e.Cursor.Col])
+	e.Dirty = true
+	e.setGoal()
+	e.followCursor()
+}
+
 // InsertRune inserts r at the cursor and advances it.
 func (e *Editor) InsertRune(r rune) {
 	rs := e.curLine()
@@ -274,6 +299,10 @@ 	case tea.KeyEnd:
 		e.MoveEnd()
 	case tea.KeyTab:
 		e.InsertRune('\t')
+	case tea.KeyCtrlU:
+		e.KillToLineStart()
+	case tea.KeyCtrlK:
+		e.KillToLineEnd()
 	}
 }
 
internal/editor/editor_test.go +36 −0
@@ -288,3 +288,39 @@ 			}
 		}
 	}
 }
+
+func TestKillToLineStart(t *testing.T) {
+	e := newEditorWith("hello world")
+	e.Cursor = Position{Row: 0, Col: 6}
+	e.KillToLineStart()
+	if e.Lines[0] != "world" {
+		t.Errorf("Lines[0] = %q, want 'world'", e.Lines[0])
+	}
+	if e.Cursor.Col != 0 {
+		t.Errorf("Cursor.Col = %d, want 0", e.Cursor.Col)
+	}
+	if !e.Dirty {
+		t.Error("kill should set Dirty")
+	}
+}
+
+func TestKillToLineEnd(t *testing.T) {
+	e := newEditorWith("hello world")
+	e.Cursor = Position{Row: 0, Col: 5}
+	e.KillToLineEnd()
+	if e.Lines[0] != "hello" {
+		t.Errorf("Lines[0] = %q, want 'hello'", e.Lines[0])
+	}
+	if !e.Dirty {
+		t.Error("kill should set Dirty")
+	}
+}
+
+func TestKillToLineStartAtColZeroNoop(t *testing.T) {
+	e := newEditorWith("abc")
+	e.Cursor = Position{Row: 0, Col: 0}
+	e.KillToLineStart()
+	if e.Lines[0] != "abc" || e.Dirty {
+		t.Errorf("kill at col 0 should be a noop; line=%q dirty=%v", e.Lines[0], e.Dirty)
+	}
+}