55
66use portable_pty:: { native_pty_system, CommandBuilder , PtySize } ;
77use std:: io:: { Read , Write } ;
8+ use std:: path:: Path ;
89use std:: thread;
910use std:: time:: Duration ;
1011use tempfile:: tempdir;
@@ -41,47 +42,74 @@ fn wait_with_timeout(child: &mut Box<dyn portable_pty::Child + Send + Sync>, tim
4142 }
4243}
4344
45+ /// Helper struct for PTY-based vi tests.
46+ struct ViPtySession {
47+ child : Box < dyn portable_pty:: Child + Send + Sync > ,
48+ writer : Box < dyn Write + Send > ,
49+ #[ allow( dead_code) ]
50+ reader_thread : thread:: JoinHandle < ( ) > ,
51+ }
52+
53+ impl ViPtySession {
54+ /// Spawn vi with the given file in a PTY of the specified size.
55+ fn new ( file_path : & Path , rows : u16 , cols : u16 ) -> Self {
56+ let pty_system = native_pty_system ( ) ;
57+ let pair = pty_system
58+ . openpty ( PtySize {
59+ rows,
60+ cols,
61+ pixel_width : 0 ,
62+ pixel_height : 0 ,
63+ } )
64+ . unwrap ( ) ;
65+
66+ let mut cmd = CommandBuilder :: new ( env ! ( "CARGO_BIN_EXE_vi" ) ) ;
67+ cmd. arg ( file_path) ;
68+ cmd. env ( "TERM" , "vt100" ) ;
69+
70+ let child = pair. slave . spawn_command ( cmd) . unwrap ( ) ;
71+ drop ( pair. slave ) ;
72+
73+ let reader = pair. master . try_clone_reader ( ) . unwrap ( ) ;
74+ let reader_thread = spawn_reader_drain ( reader) ;
75+ let writer = pair. master . take_writer ( ) . unwrap ( ) ;
76+
77+ Self {
78+ child,
79+ writer,
80+ reader_thread,
81+ }
82+ }
83+
84+ /// Send key sequence to vi.
85+ fn keys ( & mut self , s : & str ) {
86+ write_keys ( & mut self . writer , s) ;
87+ }
88+
89+ /// Sleep for the given number of milliseconds.
90+ fn sleep_ms ( & self , ms : u64 ) {
91+ thread:: sleep ( Duration :: from_millis ( ms) ) ;
92+ }
93+
94+ /// Wait for vi to exit with a timeout.
95+ fn wait ( mut self ) {
96+ wait_with_timeout ( & mut self . child , Duration :: from_secs ( 5 ) ) ;
97+ }
98+ }
99+
44100/// Test: Insert text and save file.
45101#[ test]
46102fn test_pty_vi_insert_and_save ( ) {
47103 let td = tempdir ( ) . unwrap ( ) ;
48104 let file_path = td. path ( ) . join ( "test.txt" ) ;
49105 std:: fs:: write ( & file_path, "" ) . unwrap ( ) ;
50106
51- let pty_system = native_pty_system ( ) ;
52- let pair = pty_system
53- . openpty ( PtySize {
54- rows : 25 ,
55- cols : 80 ,
56- pixel_width : 0 ,
57- pixel_height : 0 ,
58- } )
59- . unwrap ( ) ;
60-
61- let mut cmd = CommandBuilder :: new ( env ! ( "CARGO_BIN_EXE_vi" ) ) ;
62- cmd. arg ( & file_path) ;
63- cmd. env ( "TERM" , "vt100" ) ;
64-
65- let mut child = pair. slave . spawn_command ( cmd) . unwrap ( ) ;
66- drop ( pair. slave ) ;
67-
68- let reader = pair. master . try_clone_reader ( ) . unwrap ( ) ;
69- let _reader_thread = spawn_reader_drain ( reader) ;
70- let mut writer = pair. master . take_writer ( ) . unwrap ( ) ;
71-
72- // Wait for vi startup
73- thread:: sleep ( Duration :: from_millis ( 500 ) ) ;
74-
75- // Insert "Hello" and save
76- write_keys ( & mut writer, "i" ) ;
77- thread:: sleep ( Duration :: from_millis ( 50 ) ) ;
78- write_keys ( & mut writer, "Hello" ) ;
79- thread:: sleep ( Duration :: from_millis ( 50 ) ) ;
80- write_keys ( & mut writer, "\x1b " ) ; // ESC
81- thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
82- write_keys ( & mut writer, ":wq\r " ) ;
83-
84- wait_with_timeout ( & mut child, Duration :: from_secs ( 5 ) ) ;
107+ let mut vi = ViPtySession :: new ( & file_path, 25 , 80 ) ;
108+ vi. sleep_ms ( 500 ) ;
109+ vi. keys ( "iHello\x1b " ) ;
110+ vi. sleep_ms ( 100 ) ;
111+ vi. keys ( ":wq\r " ) ;
112+ vi. wait ( ) ;
85113
86114 let contents = std:: fs:: read_to_string ( & file_path) . unwrap ( ) ;
87115 assert_eq ! ( contents. trim( ) , "Hello" ) ;
@@ -94,36 +122,12 @@ fn test_pty_vi_quit_no_save() {
94122 let file_path = td. path ( ) . join ( "test.txt" ) ;
95123 std:: fs:: write ( & file_path, "original\n " ) . unwrap ( ) ;
96124
97- let pty_system = native_pty_system ( ) ;
98- let pair = pty_system
99- . openpty ( PtySize {
100- rows : 25 ,
101- cols : 80 ,
102- pixel_width : 0 ,
103- pixel_height : 0 ,
104- } )
105- . unwrap ( ) ;
106-
107- let mut cmd = CommandBuilder :: new ( env ! ( "CARGO_BIN_EXE_vi" ) ) ;
108- cmd. arg ( & file_path) ;
109- cmd. env ( "TERM" , "vt100" ) ;
110-
111- let mut child = pair. slave . spawn_command ( cmd) . unwrap ( ) ;
112- drop ( pair. slave ) ;
113-
114- let reader = pair. master . try_clone_reader ( ) . unwrap ( ) ;
115- let _reader_thread = spawn_reader_drain ( reader) ;
116- let mut writer = pair. master . take_writer ( ) . unwrap ( ) ;
117-
118- // Wait for vi startup
119- thread:: sleep ( Duration :: from_millis ( 500 ) ) ;
120-
121- // Delete line and quit without saving
122- write_keys ( & mut writer, "dd" ) ;
123- thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
124- write_keys ( & mut writer, ":q!\r " ) ;
125-
126- wait_with_timeout ( & mut child, Duration :: from_secs ( 5 ) ) ;
125+ let mut vi = ViPtySession :: new ( & file_path, 25 , 80 ) ;
126+ vi. sleep_ms ( 500 ) ;
127+ vi. keys ( "dd" ) ;
128+ vi. sleep_ms ( 100 ) ;
129+ vi. keys ( ":q!\r " ) ;
130+ vi. wait ( ) ;
127131
128132 let contents = std:: fs:: read_to_string ( & file_path) . unwrap ( ) ;
129133 assert_eq ! ( contents, "original\n " ) ;
@@ -136,40 +140,12 @@ fn test_pty_vi_multiple_lines() {
136140 let file_path = td. path ( ) . join ( "test.txt" ) ;
137141 std:: fs:: write ( & file_path, "" ) . unwrap ( ) ;
138142
139- let pty_system = native_pty_system ( ) ;
140- let pair = pty_system
141- . openpty ( PtySize {
142- rows : 25 ,
143- cols : 80 ,
144- pixel_width : 0 ,
145- pixel_height : 0 ,
146- } )
147- . unwrap ( ) ;
148-
149- let mut cmd = CommandBuilder :: new ( env ! ( "CARGO_BIN_EXE_vi" ) ) ;
150- cmd. arg ( & file_path) ;
151- cmd. env ( "TERM" , "vt100" ) ;
152-
153- let mut child = pair. slave . spawn_command ( cmd) . unwrap ( ) ;
154- drop ( pair. slave ) ;
155-
156- let reader = pair. master . try_clone_reader ( ) . unwrap ( ) ;
157- let _reader_thread = spawn_reader_drain ( reader) ;
158- let mut writer = pair. master . take_writer ( ) . unwrap ( ) ;
159-
160- // Wait for vi startup
161- thread:: sleep ( Duration :: from_millis ( 500 ) ) ;
162-
163- // Insert three lines
164- write_keys ( & mut writer, "i" ) ;
165- thread:: sleep ( Duration :: from_millis ( 50 ) ) ;
166- write_keys ( & mut writer, "Line1\r Line2\r Line3" ) ;
167- thread:: sleep ( Duration :: from_millis ( 50 ) ) ;
168- write_keys ( & mut writer, "\x1b " ) ; // ESC
169- thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
170- write_keys ( & mut writer, ":wq\r " ) ;
171-
172- wait_with_timeout ( & mut child, Duration :: from_secs ( 5 ) ) ;
143+ let mut vi = ViPtySession :: new ( & file_path, 25 , 80 ) ;
144+ vi. sleep_ms ( 500 ) ;
145+ vi. keys ( "iLine1\r Line2\r Line3\x1b " ) ;
146+ vi. sleep_ms ( 100 ) ;
147+ vi. keys ( ":wq\r " ) ;
148+ vi. wait ( ) ;
173149
174150 let contents = std:: fs:: read_to_string ( & file_path) . unwrap ( ) ;
175151 let lines: Vec < & str > = contents. lines ( ) . collect ( ) ;
@@ -186,38 +162,10 @@ fn test_pty_vi_delete_and_save() {
186162 let file_path = td. path ( ) . join ( "test.txt" ) ;
187163 std:: fs:: write ( & file_path, "Line1\n Line2\n Line3\n " ) . unwrap ( ) ;
188164
189- let pty_system = native_pty_system ( ) ;
190- let pair = pty_system
191- . openpty ( PtySize {
192- rows : 25 ,
193- cols : 80 ,
194- pixel_width : 0 ,
195- pixel_height : 0 ,
196- } )
197- . unwrap ( ) ;
198-
199- let mut cmd = CommandBuilder :: new ( env ! ( "CARGO_BIN_EXE_vi" ) ) ;
200- cmd. arg ( & file_path) ;
201- cmd. env ( "TERM" , "vt100" ) ;
202-
203- let mut child = pair. slave . spawn_command ( cmd) . unwrap ( ) ;
204- drop ( pair. slave ) ;
205-
206- let reader = pair. master . try_clone_reader ( ) . unwrap ( ) ;
207- let _reader_thread = spawn_reader_drain ( reader) ;
208- let mut writer = pair. master . take_writer ( ) . unwrap ( ) ;
209-
210- // Wait for vi startup
211- thread:: sleep ( Duration :: from_millis ( 500 ) ) ;
212-
213- // Move down one line (j), delete line (dd), save and quit
214- write_keys ( & mut writer, "j" ) ;
215- thread:: sleep ( Duration :: from_millis ( 50 ) ) ;
216- write_keys ( & mut writer, "dd" ) ;
217- thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
218- write_keys ( & mut writer, ":wq\r " ) ;
219-
220- wait_with_timeout ( & mut child, Duration :: from_secs ( 5 ) ) ;
165+ let mut vi = ViPtySession :: new ( & file_path, 25 , 80 ) ;
166+ vi. sleep_ms ( 500 ) ;
167+ vi. keys ( "jdd:wq\r " ) ;
168+ vi. wait ( ) ;
221169
222170 let contents = std:: fs:: read_to_string ( & file_path) . unwrap ( ) ;
223171 let lines: Vec < & str > = contents. lines ( ) . collect ( ) ;
@@ -235,38 +183,12 @@ fn test_pty_vi_utf8_display() {
235183 // Cyrillic text "Привет мир" = "Hello world" - each Cyrillic char is 2 bytes
236184 std:: fs:: write ( & file_path, "Привет мир\n " ) . unwrap ( ) ;
237185
238- let pty_system = native_pty_system ( ) ;
239- let pair = pty_system
240- . openpty ( PtySize {
241- rows : 10 ,
242- cols : 20 , // Narrow terminal to force truncation
243- pixel_width : 0 ,
244- pixel_height : 0 ,
245- } )
246- . unwrap ( ) ;
247-
248- let mut cmd = CommandBuilder :: new ( env ! ( "CARGO_BIN_EXE_vi" ) ) ;
249- cmd. arg ( & file_path) ;
250- cmd. env ( "TERM" , "vt100" ) ;
251-
252- let mut child = pair. slave . spawn_command ( cmd) . unwrap ( ) ;
253- drop ( pair. slave ) ;
254-
255- let reader = pair. master . try_clone_reader ( ) . unwrap ( ) ;
256- let _reader_thread = spawn_reader_drain ( reader) ;
257- let mut writer = pair. master . take_writer ( ) . unwrap ( ) ;
258-
259- // Wait for vi startup
260- thread:: sleep ( Duration :: from_millis ( 500 ) ) ;
261-
262- // Move cursor right a few times (exercises display with UTF-8)
263- write_keys ( & mut writer, "lll" ) ;
264- thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
265-
266- // Quit without saving
267- write_keys ( & mut writer, ":q!\r " ) ;
268-
269- wait_with_timeout ( & mut child, Duration :: from_secs ( 5 ) ) ;
186+ let mut vi = ViPtySession :: new ( & file_path, 10 , 20 ) ; // Narrow terminal to force truncation
187+ vi. sleep_ms ( 500 ) ;
188+ vi. keys ( "lll" ) ;
189+ vi. sleep_ms ( 100 ) ;
190+ vi. keys ( ":q!\r " ) ;
191+ vi. wait ( ) ;
270192
271193 // If we got here without panic, the test passed
272194 let contents = std:: fs:: read_to_string ( & file_path) . unwrap ( ) ;
@@ -281,46 +203,16 @@ fn test_pty_vi_set_number() {
281203 let file_path = td. path ( ) . join ( "test_number.txt" ) ;
282204 std:: fs:: write ( & file_path, "line1\n line2\n line3\n " ) . unwrap ( ) ;
283205
284- let pty_system = native_pty_system ( ) ;
285- let pair = pty_system
286- . openpty ( PtySize {
287- rows : 25 ,
288- cols : 80 ,
289- pixel_width : 0 ,
290- pixel_height : 0 ,
291- } )
292- . unwrap ( ) ;
293-
294- let mut cmd = CommandBuilder :: new ( env ! ( "CARGO_BIN_EXE_vi" ) ) ;
295- cmd. arg ( & file_path) ;
296- cmd. env ( "TERM" , "vt100" ) ;
297-
298- let mut child = pair. slave . spawn_command ( cmd) . unwrap ( ) ;
299- drop ( pair. slave ) ;
300-
301- let reader = pair. master . try_clone_reader ( ) . unwrap ( ) ;
302- let _reader_thread = spawn_reader_drain ( reader) ;
303- let mut writer = pair. master . take_writer ( ) . unwrap ( ) ;
304-
305- // Wait for vi startup
306- thread:: sleep ( Duration :: from_millis ( 500 ) ) ;
307-
308- // Enable line numbers
309- write_keys ( & mut writer, ":set number\r " ) ;
310- thread:: sleep ( Duration :: from_millis ( 200 ) ) ;
311-
312- // Move cursor to verify positioning works with line numbers
313- write_keys ( & mut writer, "jjk" ) ;
314- thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
315-
316- // Disable line numbers
317- write_keys ( & mut writer, ":set nonumber\r " ) ;
318- thread:: sleep ( Duration :: from_millis ( 100 ) ) ;
319-
320- // Quit without saving
321- write_keys ( & mut writer, ":q!\r " ) ;
322-
323- wait_with_timeout ( & mut child, Duration :: from_secs ( 5 ) ) ;
206+ let mut vi = ViPtySession :: new ( & file_path, 25 , 80 ) ;
207+ vi. sleep_ms ( 500 ) ;
208+ vi. keys ( ":set number\r " ) ;
209+ vi. sleep_ms ( 200 ) ;
210+ vi. keys ( "jjk" ) ;
211+ vi. sleep_ms ( 100 ) ;
212+ vi. keys ( ":set nonumber\r " ) ;
213+ vi. sleep_ms ( 100 ) ;
214+ vi. keys ( ":q!\r " ) ;
215+ vi. wait ( ) ;
324216
325217 // File should be unchanged
326218 let contents = std:: fs:: read_to_string ( & file_path) . unwrap ( ) ;
0 commit comments