55 "errors"
66 "fmt"
77 "io"
8- "strconv "
8+ "os "
99 "strings"
1010 "unicode/utf8"
1111
@@ -14,7 +14,7 @@ import (
1414
1515var ErrEditorCancelled = errors .New ("task entry cancelled" )
1616
17- const editorContentRow = 4
17+ const editorContentRow = 5
1818
1919type textBuffer struct {
2020 text []rune
@@ -116,25 +116,28 @@ func (reader *Reader) EditTask(output io.Writer) (string, error) {
116116 }
117117 defer term .Restore (int (reader .input .Fd ()), state )
118118
119- fmt .Fprint (output , "\x1b [?1049h\x1b [?2004h\x1b [?1000h\x1b [?1006h" )
120- defer fmt .Fprint (output , "\x1b [?1006l\x1b [?1000l\x1b [?2004l\x1b [?1049l" )
119+ // >4;2m and >1u ask the terminal to report Shift+Enter distinctly from
120+ // Enter (xterm modifyOtherKeys and the Kitty keyboard protocol,
121+ // respectively); terminals that don't understand them ignore them.
122+ fmt .Fprint (output , "\x1b [?1049h\x1b [?2004h\x1b [>4;2m\x1b [>1u" )
123+ defer fmt .Fprint (output , "\x1b [<u\x1b [>4;0m\x1b [?2004l\x1b [?1049l" )
121124
125+ folder := workingDirectory ()
122126 buffer := & textBuffer {}
123127 for {
124- drawEditor (output , buffer )
128+ drawEditor (output , buffer , folder )
125129 key , err := reader .buffer .ReadByte ()
126130 if err != nil {
127131 return "" , ErrEditorCancelled
128132 }
129133 switch key {
130134 case 3 :
131135 return "" , ErrEditorCancelled
132- case 4 :
133- task := strings .TrimSpace (string (buffer .text ))
134- if task != "" {
136+ case 4 , '\r' :
137+ if task , ok := submit (buffer ); ok {
135138 return task , nil
136139 }
137- case '\r' , '\ n' :
140+ case '\n' :
138141 buffer .insert ("\n " )
139142 case 8 , 127 :
140143 buffer .backspace ()
@@ -143,9 +146,15 @@ func (reader *Reader) EditTask(output io.Writer) (string, error) {
143146 case 5 :
144147 buffer .end ()
145148 case 27 :
146- if err := handleEditorEscape (reader .buffer , buffer ); err != nil {
149+ enter , err := handleEditorEscape (reader .buffer , buffer )
150+ if err != nil {
147151 return "" , err
148152 }
153+ if enter {
154+ if task , ok := submit (buffer ); ok {
155+ return task , nil
156+ }
157+ }
149158 default :
150159 character , err := readRune (reader .buffer , key )
151160 if err == nil && character >= ' ' {
@@ -155,10 +164,29 @@ func (reader *Reader) EditTask(output io.Writer) (string, error) {
155164 }
156165}
157166
158- func drawEditor (output io.Writer , buffer * textBuffer ) {
167+ func submit (buffer * textBuffer ) (string , bool ) {
168+ task := strings .TrimSpace (string (buffer .text ))
169+ return task , task != ""
170+ }
171+
172+ func workingDirectory () string {
173+ directory , err := os .Getwd ()
174+ if err != nil {
175+ return ""
176+ }
177+ if home , err := os .UserHomeDir (); err == nil {
178+ if relative , ok := strings .CutPrefix (directory , home ); ok {
179+ directory = "~" + relative
180+ }
181+ }
182+ return directory
183+ }
184+
185+ func drawEditor (output io.Writer , buffer * textBuffer , folder string ) {
159186 fmt .Fprint (output , "\x1b [H\x1b [2J" )
187+ fmt .Fprintf (output , "\x1b [36m[^_^] YoloCoder\x1b [0m \x1b [2m%s\x1b [0m\r \n " , folder )
160188 fmt .Fprint (output , "Describe the coding task\r \n " )
161- fmt .Fprint (output , "\x1b [2mEnter: new line • Ctrl+D: submit • Ctrl+C: cancel • arrows/click : move cursor\x1b [0m\r \n \r \n " )
189+ fmt .Fprint (output , "\x1b [2mEnter: send • Shift+Enter: new line • Ctrl+C: cancel • arrows: move cursor\x1b [0m\r \n \r \n " )
162190 lines := strings .Split (string (buffer .text ), "\n " )
163191 for _ , line := range lines {
164192 fmt .Fprintf (output , "\x1b [36m│\x1b [0m %s\x1b [K\r \n " , line )
@@ -167,16 +195,31 @@ func drawEditor(output io.Writer, buffer *textBuffer) {
167195 fmt .Fprintf (output , "\x1b [%d;%dH" , editorContentRow + row , 3 + column )
168196}
169197
170- func handleEditorEscape (reader * bufio.Reader , buffer * textBuffer ) error {
198+ // handleEditorEscape parses one CSI sequence following an ESC byte and
199+ // applies it to buffer. It reports enter=true when the sequence represents
200+ // a plain Enter key reported through the CSI u keyboard protocol.
201+ func handleEditorEscape (reader * bufio.Reader , buffer * textBuffer ) (bool , error ) {
171202 next , err := reader .ReadByte ()
172- if err != nil || next != '[' {
173- return nil
203+ if err != nil {
204+ return false , ErrEditorCancelled
174205 }
175- code , err := reader .ReadByte ()
206+ if next != '[' {
207+ return false , nil
208+ }
209+ first , err := reader .ReadByte ()
176210 if err != nil {
177- return ErrEditorCancelled
211+ return false , ErrEditorCancelled
212+ }
213+ var params strings.Builder
214+ final := first
215+ for final < 0x40 || final > 0x7e {
216+ params .WriteByte (final )
217+ final , err = reader .ReadByte ()
218+ if err != nil {
219+ return false , ErrEditorCancelled
220+ }
178221 }
179- switch code {
222+ switch final {
180223 case 'A' :
181224 buffer .moveVertical (- 1 )
182225 case 'B' :
@@ -189,44 +232,36 @@ func handleEditorEscape(reader *bufio.Reader, buffer *textBuffer) error {
189232 buffer .home ()
190233 case 'F' :
191234 buffer .end ()
192- case '3' :
193- if terminator , _ := reader .ReadByte (); terminator == '~' {
235+ case '~' :
236+ switch params .String () {
237+ case "3" :
194238 buffer .delete ()
195- }
196- case '2' :
197- sequence , err := readControlSequence (reader , '~' )
198- if err != nil {
199- return err
200- }
201- if sequence == "00" {
239+ case "200" :
202240 pasted , err := readBracketedPaste (reader )
203241 if err != nil {
204- return err
242+ return false , err
205243 }
206244 buffer .insert (pasted )
207245 }
208- case '<' :
209- sequence , err := readMouseSequence (reader )
210- if err != nil {
211- return err
212- }
213- positionMouse (buffer , sequence )
246+ case 'u' :
247+ return handleKeyboardProtocol (params .String (), buffer ), nil
214248 }
215- return nil
249+ return false , nil
216250}
217251
218- func readControlSequence ( reader * bufio. Reader , terminator byte ) ( string , error ) {
219- var sequence strings. Builder
220- for {
221- character , err := reader . ReadByte ()
222- if err != nil {
223- return "" , ErrEditorCancelled
224- }
225- if character == terminator {
226- return sequence . String (), nil
227- }
228- sequence . WriteByte ( character )
252+ // handleKeyboardProtocol interprets a CSI u sequence ("codepoint;modifiers")
253+ // sent by terminals that support xterm's modifyOtherKeys or the Kitty
254+ // keyboard protocol. It reports enter=true for a plain Enter key press.
255+ func handleKeyboardProtocol ( params string , buffer * textBuffer ) bool {
256+ codepoint , modifiers , _ := strings . Cut ( params , ";" )
257+ if codepoint != "13" {
258+ return false
259+ }
260+ if modifiers == "2" {
261+ buffer . insert ( " \n " )
262+ return false
229263 }
264+ return true
230265}
231266
232267func readBracketedPaste (reader * bufio.Reader ) (string , error ) {
@@ -250,36 +285,6 @@ func readBracketedPaste(reader *bufio.Reader) (string, error) {
250285 }
251286}
252287
253- func readMouseSequence (reader * bufio.Reader ) (string , error ) {
254- var sequence strings.Builder
255- for {
256- character , err := reader .ReadByte ()
257- if err != nil {
258- return "" , ErrEditorCancelled
259- }
260- sequence .WriteByte (character )
261- if character == 'M' || character == 'm' {
262- return sequence .String (), nil
263- }
264- }
265- }
266-
267- func positionMouse (buffer * textBuffer , sequence string ) {
268- if ! strings .HasSuffix (sequence , "M" ) {
269- return
270- }
271- fields := strings .Split (strings .TrimSuffix (sequence , "M" ), ";" )
272- if len (fields ) != 3 || fields [0 ] != "0" {
273- return
274- }
275- column , columnErr := strconv .Atoi (fields [1 ])
276- row , rowErr := strconv .Atoi (fields [2 ])
277- if columnErr != nil || rowErr != nil || row < editorContentRow {
278- return
279- }
280- buffer .setPosition (row - editorContentRow , column - 3 )
281- }
282-
283288func readRune (reader * bufio.Reader , first byte ) (rune , error ) {
284289 if first < utf8 .RuneSelf {
285290 return rune (first ), nil
0 commit comments