@@ -194,6 +194,11 @@ pub const prompt = struct {
194194 }
195195
196196 /// https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-prompt
197+ /// This implementation has two modes:
198+ /// 1. If stdin is an interactive TTY, it switches the terminal to raw mode to
199+ /// provide a rich editing experience with cursor movement and history.
200+ /// 2. If stdin is not a TTY (e.g., piped input), it falls back to a simple
201+ /// buffered line reader.
197202 pub fn call (
198203 globalObject : * jsc.JSGlobalObject ,
199204 callframe : * jsc.CallFrame ,
@@ -231,7 +236,7 @@ pub const prompt = struct {
231236 // default.
232237 output .writeAll (if (has_message ) " " else "Prompt " ) catch {
233238 // 1. If we cannot show simple dialogs for this, then return false.
234- return .false ;
239+ return .null ;
235240 };
236241
237242 if (has_default ) {
@@ -240,7 +245,7 @@ pub const prompt = struct {
240245
241246 output .print ("[{s}] " , .{default_string .slice ()}) catch {
242247 // 1. If we cannot show simple dialogs for this, then return false.
243- return .false ;
248+ return .null ;
244249 };
245250 }
246251
@@ -260,6 +265,150 @@ pub const prompt = struct {
260265 };
261266
262267 // 7. Pause while waiting for the user's response.
268+ if (comptime ! Environment .isWindows ) {
269+ const c_termios = @cImport ({
270+ @cInclude ("termios.h" );
271+ @cInclude ("unistd.h" );
272+ @cInclude ("signal.h" );
273+ });
274+
275+ if (c_termios .isatty (bun .FD .stdin ().native ()) == 1 ) {
276+ var original_termios : c_termios.termios = undefined ;
277+ if (c_termios .tcgetattr (bun .FD .stdin ().native (), & original_termios ) != 0 ) {
278+ return .null ;
279+ }
280+
281+ defer {
282+ _ = c_termios .tcsetattr (bun .FD .stdin ().native (), c_termios .TCSADRAIN , & original_termios );
283+ // Move cursor to next line after input is done
284+ _ = bun .Output .writer ().writeAll ("\n " ) catch {};
285+ bun .Output .flush ();
286+ }
287+
288+ var raw_termios = original_termios ;
289+ // Unset canonical mode and echo
290+ raw_termios .c_lflag &= ~ @as (c_termios .tcflag_t , c_termios .ICANON | c_termios .ECHO );
291+
292+ if (c_termios .tcsetattr (bun .FD .stdin ().native (), c_termios .TCSADRAIN , & raw_termios ) != 0 ) {
293+ return .null ;
294+ }
295+
296+ var input = std .ArrayList (u8 ).init (allocator );
297+ defer input .deinit ();
298+ var cursor_index : usize = 0 ;
299+
300+ const reader = bun .Output .buffered_stdin .reader ();
301+ var stdout_writer = bun .Output .writer ();
302+
303+ while (true ) {
304+ const byte = reader .readByte () catch {
305+ // User aborted (Ctrl+D)
306+ return .null ;
307+ };
308+
309+ switch (byte ) {
310+ // End of input
311+ '\n ' , '\r ' = > {
312+ if (input .items .len == 0 and ! has_default ) return jsc .ZigString .init ("" ).toJS (globalObject );
313+ if (input .items .len == 0 ) return default ;
314+
315+ var result = jsc .ZigString .init (input .items );
316+ result .markUTF8 ();
317+ return result .toJS (globalObject );
318+ },
319+
320+ // Backspace (ASCII 8) or DEL (ASCII 127)
321+ 8 , 127 = > {
322+ if (cursor_index > 0 ) {
323+ _ = input .orderedRemove (cursor_index - 1 );
324+ cursor_index -= 1 ;
325+
326+ // Redraw the line from the cursor
327+ _ = stdout_writer .writeAll ("\x1b [D" ) catch {}; // Move cursor left
328+ _ = stdout_writer .writeAll (input .items [cursor_index .. ]) catch {};
329+ _ = stdout_writer .writeAll (" " ) catch {}; // Clear the character at the end
330+ _ = stdout_writer .print ("\x1b [{d}D" , .{input .items .len - cursor_index + 1 }) catch {}; // Move cursor back
331+ bun .Output .flush ();
332+ }
333+ },
334+
335+ // Ctrl+C
336+ 3 = > {
337+ // This will trigger the defer and restore terminal settings
338+ _ = c_termios .raise (c_termios .SIGINT );
339+ return .null ;
340+ },
341+
342+ // Escape sequence (e.g., arrow keys)
343+ 27 = > {
344+ // Try to read the next two bytes for [D (left) or [C (right)
345+ const byte2 = reader .readByte () catch continue ;
346+ if (byte2 != '[' ) {
347+ continue ;
348+ }
349+ switch (reader .readByte () catch continue ) {
350+ 'D' = > { // Left arrow
351+ if (cursor_index > 0 ) {
352+ cursor_index -= 1 ;
353+ _ = stdout_writer .writeAll ("\x1b [D" ) catch {};
354+ bun .Output .flush ();
355+ }
356+ },
357+ 'C' = > { // Right arrow
358+ if (cursor_index < input .items .len ) {
359+ cursor_index += 1 ;
360+ _ = stdout_writer .writeAll ("\x1b [C" ) catch {};
361+ bun .Output .flush ();
362+ }
363+ },
364+ '3' = > { // DEL
365+ const next = reader .readByte () catch continue ;
366+ if (next != '~' ) {
367+ // Signifies that there is a modifier key (SHIFT, CTRL).
368+ // We ignore the modifier as that is what canonical mode does.
369+ if (next == ';' ) {
370+ _ = reader .readByte () catch continue ; // modifier key skipped
371+ const final = reader .readByte () catch continue ;
372+ if (final != '~' ) {
373+ continue ;
374+ }
375+ } else {
376+ continue ;
377+ }
378+ }
379+ // Handle Delete key: remove character under cursor
380+ if (cursor_index < input .items .len ) {
381+ _ = input .orderedRemove (cursor_index );
382+
383+ // Redraw from cursor
384+ _ = stdout_writer .writeAll (input .items [cursor_index .. ]) catch {};
385+ _ = stdout_writer .writeAll (" " ) catch {};
386+ _ = stdout_writer .print ("\x1b [{d}D" , .{input .items .len - cursor_index + 1 }) catch {};
387+ bun .Output .flush ();
388+ }
389+ },
390+ else = > {},
391+ }
392+ },
393+
394+ else = > {
395+ try input .insert (cursor_index , byte );
396+ cursor_index += 1 ;
397+
398+ // Echo the new character and redraw the rest of the line
399+ _ = stdout_writer .writeAll (input .items [cursor_index - 1 .. ]) catch {};
400+ // Move cursor back to its correct position
401+ if (input .items .len > cursor_index ) {
402+ _ = stdout_writer .print ("\x1b [{d}D" , .{input .items .len - cursor_index }) catch {};
403+ }
404+ bun .Output .flush ();
405+ },
406+ }
407+ }
408+ }
409+ }
410+
411+ // Fallback for non-interactive terminals (or Windows)
263412 const reader = bun .Output .buffered_stdin .reader ();
264413 var second_byte : ? u8 = null ;
265414 const first_byte = reader .readByte () catch {
0 commit comments