@@ -3913,6 +3913,45 @@ pub fn fullReset(self: *Terminal) void {
39133913 self .flags .dirty .clear = true ;
39143914}
39153915
3916+ /// Soft terminal reset (DECSTR, `CSI ! p`). Resets programmable state (modes,
3917+ /// SGR, charsets, margins, saved cursor) to defaults without touching the
3918+ /// screen or cursor. This is the difference from `fullReset` (RIS).
3919+ pub fn softReset (self : * Terminal ) void {
3920+ const screen : * Screen = self .screens .active ;
3921+
3922+ // Modes reset by DECSTR (DEC STD 070).
3923+ self .modes .set (.cursor_visible , true ); // DECTCEM
3924+ self .modes .set (.insert , false ); // IRM
3925+ self .modes .set (.origin , false ); // DECOM
3926+ self .modes .set (.disable_keyboard , false ); // KAM
3927+ self .modes .set (.cursor_keys , false ); // DECCKM
3928+ self .modes .set (.keypad_keys , false ); // DECNKM
3929+ // DECAWM: the spec resets this off, but we match xterm/conhost and leave
3930+ // the default (on).
3931+ self .modes .set (.wraparound , true );
3932+
3933+ // SGR back to default.
3934+ screen .cursor .style = .{};
3935+ screen .manualStyleUpdate () catch unreachable ;
3936+
3937+ // Charsets (G0-G3) back to ASCII.
3938+ screen .charset = .{};
3939+
3940+ self .setProtectedMode (.off ); // DECSCA
3941+
3942+ // Full scrolling region.
3943+ self .scrolling_region = .{
3944+ .top = 0 ,
3945+ .bottom = self .rows - 1 ,
3946+ .left = 0 ,
3947+ .right = self .cols - 1 ,
3948+ };
3949+
3950+ screen .cursor .pending_wrap = false ;
3951+ screen .saved_cursor = null ; // DECSC
3952+ self .previous_char = null ; // REP
3953+ }
3954+
39163955/// Returns true if the point is dirty, used for testing.
39173956fn isDirty (t : * const Terminal , pt : point.Point ) bool {
39183957 return t .screens .active .pages .getCell (pt ).? .isDirty ();
@@ -13777,6 +13816,71 @@ test "Terminal: fullReset origin mode" {
1377713816 try testing .expect (! t .modes .get (.origin ));
1377813817}
1377913818
13819+ test "Terminal: softReset resets state without clearing the screen" {
13820+ var t = try init (testing .allocator , .{ .cols = 10 , .rows = 10 });
13821+ defer t .deinit (testing .allocator );
13822+
13823+ // Put content on the screen so we can confirm it survives.
13824+ try t .printString ("hello" );
13825+
13826+ // Pile on non-default programmable state.
13827+ t .modes .set (.origin , true );
13828+ t .modes .set (.insert , true );
13829+ t .modes .set (.wraparound , false );
13830+ t .modes .set (.cursor_visible , false );
13831+ t .modes .set (.disable_keyboard , true );
13832+ t .modes .set (.cursor_keys , true );
13833+ t .modes .set (.keypad_keys , true );
13834+ t .scrolling_region = .{ .top = 2 , .bottom = 5 , .left = 0 , .right = 9 };
13835+ t .configureCharset (.G0 , .dec_special );
13836+ t .setProtectedMode (.iso );
13837+ t .saveCursor ();
13838+ try t .setAttribute (.{ .bold = {} });
13839+
13840+ // DECSTR must not move the cursor (unlike RIS).
13841+ const cursor_x = t .screens .active .cursor .x ;
13842+ const cursor_y = t .screens .active .cursor .y ;
13843+
13844+ t .softReset ();
13845+
13846+ // Modes back to their defaults.
13847+ try testing .expect (t .modes .get (.cursor_visible ));
13848+ try testing .expect (! t .modes .get (.insert ));
13849+ try testing .expect (! t .modes .get (.origin ));
13850+ try testing .expect (t .modes .get (.wraparound ));
13851+ try testing .expect (! t .modes .get (.disable_keyboard ));
13852+ try testing .expect (! t .modes .get (.cursor_keys ));
13853+ try testing .expect (! t .modes .get (.keypad_keys ));
13854+
13855+ // Cursor position is unchanged.
13856+ try testing .expectEqual (cursor_x , t .screens .active .cursor .x );
13857+ try testing .expectEqual (cursor_y , t .screens .active .cursor .y );
13858+
13859+ // Scrolling region restored to the full screen.
13860+ try testing .expectEqual (@as (usize , 0 ), t .scrolling_region .top );
13861+ try testing .expectEqual (@as (usize , 9 ), t .scrolling_region .bottom );
13862+
13863+ // SGR back to normal rendition.
13864+ try testing .expect (! t .screens .active .cursor .style .flags .bold );
13865+
13866+ // Character set reset (no pending single shift after G0->ASCII).
13867+ try testing .expect (t .screens .active .charset .single_shift == null );
13868+
13869+ // Protected-character bit cleared.
13870+ try testing .expect (! t .screens .active .cursor .protected );
13871+
13872+ // Saved cursor (DECSC) cleared.
13873+ try testing .expect (t .screens .active .saved_cursor == null );
13874+
13875+ // The screen content is preserved — this is the key difference from
13876+ // fullReset (RIS), which clears it.
13877+ {
13878+ const str = try t .plainString (testing .allocator );
13879+ defer testing .allocator .free (str );
13880+ try testing .expectEqualStrings ("hello" , str );
13881+ }
13882+ }
13883+
1378013884test "Terminal: fullReset status display" {
1378113885 var t = try init (testing .allocator , .{ .cols = 10 , .rows = 10 });
1378213886 defer t .deinit (testing .allocator );
0 commit comments