@@ -10,9 +10,15 @@ import (
1010 "github.com/GrayCodeAI/iterate/internal/ui"
1111)
1212
13+ // inMultilineBlock is set when the user opens a triple-backtick block.
14+ // It is cleared when the closing ``` is entered or the input is cancelled.
15+ var inMultilineBlock bool
16+
1317// ReadInput reads user input in raw mode.
14- // Enter submits. Shift+Enter adds a newline. Up/Down arrow navigates history.
15- // Left/Right arrows move the cursor. Delete removes the char under the cursor.
18+ // Enter submits. Ctrl+J inserts a literal newline (multiline input).
19+ // Starting a line with ``` enters block mode; another ``` closes it.
20+ // Up/Down arrow navigates history. Left/Right arrows move the cursor.
21+ // Delete removes the char under the cursor.
1622// Returns (text, true) or ("", false) on Ctrl+C/EOF.
1723func ReadInput () (string , bool ) {
1824 fd := int (os .Stdin .Fd ())
@@ -70,6 +76,44 @@ func ReadInput() (string, bool) {
7076 }
7177}
7278
79+ // moveWordBackward moves cursorPos back to the start of the previous word and
80+ // emits the required backspace characters so the terminal cursor follows.
81+ func moveWordBackward (buf * []byte , cursorPos * int ) {
82+ i := * cursorPos
83+ // Skip trailing spaces.
84+ for i > 0 && (* buf )[i - 1 ] == ' ' {
85+ i --
86+ }
87+ // Skip the word.
88+ for i > 0 && (* buf )[i - 1 ] != ' ' {
89+ i --
90+ }
91+ delta := * cursorPos - i
92+ if delta > 0 {
93+ fmt .Printf ("%s" , strings .Repeat ("\b " , delta ))
94+ * cursorPos = i
95+ }
96+ }
97+
98+ // moveWordForward moves cursorPos to the end of the next word and emits the
99+ // required character output so the terminal cursor follows.
100+ func moveWordForward (buf * []byte , cursorPos * int ) {
101+ i := * cursorPos
102+ n := len (* buf )
103+ // Skip leading spaces.
104+ for i < n && (* buf )[i ] == ' ' {
105+ i ++
106+ }
107+ // Skip the word.
108+ for i < n && (* buf )[i ] != ' ' {
109+ i ++
110+ }
111+ if i > * cursorPos {
112+ fmt .Printf ("%s" , string ((* buf )[* cursorPos :i ]))
113+ * cursorPos = i
114+ }
115+ }
116+
73117// redrawFromCursor redraws buf[cursorPos:] on the terminal, appends erased
74118// spaces to clear old characters, then repositions the cursor back to cursorPos.
75119func redrawFromCursor (buf []byte , cursorPos , erased int ) {
@@ -81,12 +125,25 @@ func redrawFromCursor(buf []byte, cursorPos, erased int) {
81125// handleRawInput processes a single raw-mode key event and returns (done, text, ok).
82126func handleRawInput (b []byte , n int , buf * []byte , cursorPos * int , histSnapshot * []string , histIdx * int , savedBuf * []byte , killRing * string , replacePrompt func (string ), fd int , oldState * term.State ) (done bool , result string , ok bool ) {
83127 switch {
84- case b [0 ] == '\r' || b [ 0 ] == '\n' :
128+ case b [0 ] == '\r' :
85129 return handleLineSubmit (buf , cursorPos , histSnapshot , histIdx , savedBuf )
86130
131+ case b [0 ] == '\n' : // Ctrl+J — insert literal newline (multiline input)
132+ * buf = append (append ((* buf )[:* cursorPos :* cursorPos ], '\n' ), (* buf )[* cursorPos :]... )
133+ * cursorPos ++
134+ fmt .Print ("\r \n " )
135+ fmt .Printf ("%s ...%s " , ui .ColorDim , ui .ColorReset )
136+ // Reprint any text after the cursor on the new line.
137+ if * cursorPos < len (* buf ) {
138+ fmt .Printf ("%s" , string ((* buf )[* cursorPos :]))
139+ fmt .Printf ("%s" , strings .Repeat ("\b " , len (* buf )- * cursorPos ))
140+ }
141+ return false , "" , false
142+
87143 case b [0 ] == 3 :
88144 // Two-stage Ctrl+C: first press clears the line, second press exits.
89- if len (* buf ) > 0 {
145+ if len (* buf ) > 0 || inMultilineBlock {
146+ inMultilineBlock = false
90147 if * cursorPos > 0 {
91148 fmt .Printf ("%s" , strings .Repeat ("\b " , * cursorPos ))
92149 }
@@ -115,6 +172,16 @@ func handleRawInput(b []byte, n int, buf *[]byte, cursorPos *int, histSnapshot *
115172 handleEscSeq (b , n , buf , cursorPos , histSnapshot , histIdx , savedBuf , replacePrompt )
116173 return false , "" , false
117174
175+ case b [0 ] == 27 && n == 2 && b [1 ] == 'b' :
176+ // Alt+Left (ESC b) — move backward one word.
177+ moveWordBackward (buf , cursorPos )
178+ return false , "" , false
179+
180+ case b [0 ] == 27 && n == 2 && b [1 ] == 'f' :
181+ // Alt+Right (ESC f) — move forward one word.
182+ moveWordForward (buf , cursorPos )
183+ return false , "" , false
184+
118185 case b [0 ] == 27 && n == 1 :
119186 return false , "" , false
120187
@@ -259,10 +326,45 @@ func handleEscSeq(b []byte, n int, buf *[]byte, cursorPos *int, histSnapshot *[]
259326 }
260327}
261328
262- // handleLineSubmit processes Enter key, handling backslash continuation.
329+ // handleLineSubmit processes Enter key, handling backslash continuation and
330+ // triple-backtick multiline blocks.
263331func handleLineSubmit (buf * []byte , cursorPos * int , histSnapshot * []string , histIdx * int , savedBuf * []byte ) (done bool , result string , ok bool ) {
264332 fmt .Print ("\r \n " )
265333 text := string (* buf )
334+ trimmedText := strings .TrimSpace (text )
335+
336+ // Inside a triple-backtick block: ``` closes it, otherwise accumulate.
337+ if inMultilineBlock {
338+ if trimmedText == "```" {
339+ // Close the block — strip the closing fence and submit accumulated content.
340+ inMultilineBlock = false
341+ // Remove the closing ``` from the buffer tail.
342+ idx := strings .LastIndex (text , "```" )
343+ if idx >= 0 {
344+ text = strings .TrimRight (text [:idx ], "\n " )
345+ }
346+ appendHistory (text )
347+ return true , text , true
348+ }
349+ // Continue accumulating.
350+ text = text + "\n "
351+ * buf = []byte (text )
352+ * cursorPos = len (* buf )
353+ fmt .Printf ("%s ...%s " , ui .ColorDim , ui .ColorReset )
354+ return false , "" , false
355+ }
356+
357+ // Opening triple-backtick block.
358+ if trimmedText == "```" {
359+ inMultilineBlock = true
360+ fmt .Printf ("%s (multiline block — end with a line containing only ``` )%s\n " , ui .ColorDim , ui .ColorReset )
361+ fmt .Printf ("%s ...%s " , ui .ColorDim , ui .ColorReset )
362+ * buf = []byte {}
363+ * cursorPos = 0
364+ return false , "" , false
365+ }
366+
367+ // Backslash continuation: "some text\" → append newline and keep reading.
266368 if strings .HasSuffix (strings .TrimRight (text , " " ), "\\ " ) {
267369 text = strings .TrimRight (text , " " )
268370 text = text [:len (text )- 1 ] + "\n "
@@ -271,6 +373,16 @@ func handleLineSubmit(buf *[]byte, cursorPos *int, histSnapshot *[]string, histI
271373 fmt .Printf ("%s ...%s " , ui .ColorDim , ui .ColorReset )
272374 return false , "" , false
273375 }
376+
377+ // Already in backslash continuation (buf has embedded newlines from prior \-Enter)?
378+ if strings .Contains (text , "\n " ) {
379+ // Keep accumulating: no trailing \ means next Enter will submit.
380+ // (The user can end by pressing Enter without a trailing \.)
381+ text = strings .TrimSpace (text )
382+ appendHistory (text )
383+ return true , text , true
384+ }
385+
274386 text = strings .TrimSpace (text )
275387 appendHistory (text )
276388 return true , text , true
0 commit comments