1 //! WinApi related logic for terminal manipulation.
2 use crossterm_winapi::{Console, ConsoleMode, Coord, Handle, ScreenBuffer, Size};
3 use winapi::{
4     shared::minwindef::DWORD,
5     um::wincon::{SetConsoleTitleW, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT},
6 };
7 
8 use crate::{cursor, terminal::ClearType, ErrorKind, Result};
9 
10 const RAW_MODE_MASK: DWORD = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT;
11 
enable_raw_mode() -> Result<()>12 pub(crate) fn enable_raw_mode() -> Result<()> {
13     let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
14 
15     let dw_mode = console_mode.mode()?;
16 
17     let new_mode = dw_mode & !RAW_MODE_MASK;
18 
19     console_mode.set_mode(new_mode)?;
20 
21     Ok(())
22 }
23 
disable_raw_mode() -> Result<()>24 pub(crate) fn disable_raw_mode() -> Result<()> {
25     let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
26 
27     let dw_mode = console_mode.mode()?;
28 
29     let new_mode = dw_mode | RAW_MODE_MASK;
30 
31     console_mode.set_mode(new_mode)?;
32 
33     Ok(())
34 }
35 
size() -> Result<(u16, u16)>36 pub(crate) fn size() -> Result<(u16, u16)> {
37     let terminal_size = ScreenBuffer::current()?.info()?.terminal_size();
38     // windows starts counting at 0, unix at 1, add one to replicated unix behaviour.
39     Ok((
40         (terminal_size.width + 1) as u16,
41         (terminal_size.height + 1) as u16,
42     ))
43 }
44 
clear(clear_type: ClearType) -> Result<()>45 pub(crate) fn clear(clear_type: ClearType) -> Result<()> {
46     let screen_buffer = ScreenBuffer::current()?;
47     let csbi = screen_buffer.info()?;
48 
49     let pos = csbi.cursor_pos();
50     let buffer_size = csbi.buffer_size();
51     let current_attribute = csbi.attributes();
52 
53     match clear_type {
54         ClearType::All => {
55             clear_entire_screen(buffer_size, current_attribute)?;
56         }
57         ClearType::FromCursorDown => clear_after_cursor(pos, buffer_size, current_attribute)?,
58         ClearType::FromCursorUp => clear_before_cursor(pos, buffer_size, current_attribute)?,
59         ClearType::CurrentLine => clear_current_line(pos, buffer_size, current_attribute)?,
60         ClearType::UntilNewLine => clear_until_line(pos, buffer_size, current_attribute)?,
61     };
62     Ok(())
63 }
64 
scroll_up(row_count: u16) -> Result<()>65 pub(crate) fn scroll_up(row_count: u16) -> Result<()> {
66     let csbi = ScreenBuffer::current()?;
67     let mut window = csbi.info()?.terminal_window();
68 
69     // check whether the window is too close to the screen buffer top
70     let count = row_count as i16;
71     if window.top >= count {
72         window.top -= count; // move top down
73         window.bottom -= count; // move bottom down
74 
75         Console::output()?.set_console_info(true, window)?;
76     }
77     Ok(())
78 }
79 
scroll_down(row_count: u16) -> Result<()>80 pub(crate) fn scroll_down(row_count: u16) -> Result<()> {
81     let screen_buffer = ScreenBuffer::current()?;
82     let csbi = screen_buffer.info()?;
83     let mut window = csbi.terminal_window();
84     let buffer_size = csbi.buffer_size();
85 
86     // check whether the window is too close to the screen buffer top
87     let count = row_count as i16;
88     if window.bottom < buffer_size.height - count {
89         window.top += count; // move top down
90         window.bottom += count; // move bottom down
91 
92         Console::output()?.set_console_info(true, window)?;
93     }
94     Ok(())
95 }
96 
set_size(width: u16, height: u16) -> Result<()>97 pub(crate) fn set_size(width: u16, height: u16) -> Result<()> {
98     if width <= 1 {
99         return Err(ErrorKind::ResizingTerminalFailure(String::from(
100             "Cannot set the terminal width lower than 1.",
101         )));
102     }
103 
104     if height <= 1 {
105         return Err(ErrorKind::ResizingTerminalFailure(String::from(
106             "Cannot set the terminal height lower then 1.",
107         )));
108     }
109 
110     // get the position of the current console window
111     let screen_buffer = ScreenBuffer::current()?;
112     let console = Console::from(screen_buffer.handle().clone());
113     let csbi = screen_buffer.info()?;
114 
115     let current_size = csbi.buffer_size();
116     let window = csbi.terminal_window();
117 
118     let mut new_size = Size::new(current_size.width, current_size.height);
119 
120     // If the buffer is smaller than this new window size, resize the
121     // buffer to be large enough.  Include window position.
122     let mut resize_buffer = false;
123 
124     let width = width as i16;
125     if current_size.width < window.left + width {
126         if window.left >= i16::max_value() - width {
127             return Err(ErrorKind::ResizingTerminalFailure(String::from(
128                 "Argument out of range when setting terminal width.",
129             )));
130         }
131 
132         new_size.width = window.left + width;
133         resize_buffer = true;
134     }
135     let height = height as i16;
136     if current_size.height < window.top + height {
137         if window.top >= i16::max_value() - height {
138             return Err(ErrorKind::ResizingTerminalFailure(String::from(
139                 "Argument out of range when setting terminal height.",
140             )));
141         }
142 
143         new_size.height = window.top + height;
144         resize_buffer = true;
145     }
146 
147     if resize_buffer
148         && screen_buffer
149             .set_size(new_size.width - 1, new_size.height - 1)
150             .is_err()
151     {
152         return Err(ErrorKind::ResizingTerminalFailure(String::from(
153             "Something went wrong when setting screen buffer size.",
154         )));
155     }
156 
157     let mut window = window;
158 
159     // preserve the position, but change the size.
160     window.bottom = window.top + height - 1;
161     window.right = window.left + width - 1;
162     console.set_console_info(true, window)?;
163 
164     // if we resized the buffer, un-resize it.
165     if resize_buffer
166         && screen_buffer
167             .set_size(current_size.width - 1, current_size.height - 1)
168             .is_err()
169     {
170         return Err(ErrorKind::ResizingTerminalFailure(String::from(
171             "Something went wrong when setting screen buffer size.",
172         )));
173     }
174 
175     let bounds = console.largest_window_size();
176 
177     if width > bounds.x {
178         return Err(ErrorKind::ResizingTerminalFailure(format!(
179             "Argument width: {} out of range when setting terminal width.",
180             width
181         )));
182     }
183     if height > bounds.y {
184         return Err(ErrorKind::ResizingTerminalFailure(format!(
185             "Argument height: {} out of range when setting terminal height.",
186             width
187         )));
188     }
189 
190     Ok(())
191 }
192 
set_window_title(title: &str) -> Result<()>193 pub(crate) fn set_window_title(title: &str) -> Result<()> {
194     let mut title: Vec<_> = title.encode_utf16().collect();
195     title.push(0);
196     let result = unsafe { SetConsoleTitleW(title.as_ptr()) };
197     if result != 0 {
198         Ok(())
199     } else {
200         Err(ErrorKind::SettingTerminalTitleFailure)
201     }
202 }
203 
clear_after_cursor(location: Coord, buffer_size: Size, current_attribute: u16) -> Result<()>204 fn clear_after_cursor(location: Coord, buffer_size: Size, current_attribute: u16) -> Result<()> {
205     let (mut x, mut y) = (location.x, location.y);
206 
207     // if cursor position is at the outer right position
208     if x as i16 > buffer_size.width {
209         y += 1;
210         x = 0;
211     }
212 
213     // location where to start clearing
214     let start_location = Coord::new(x, y);
215 
216     // get sum cells before cursor
217     let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32;
218 
219     clear_winapi(start_location, cells_to_write, current_attribute)
220 }
221 
clear_before_cursor(location: Coord, buffer_size: Size, current_attribute: u16) -> Result<()>222 fn clear_before_cursor(location: Coord, buffer_size: Size, current_attribute: u16) -> Result<()> {
223     let (xpos, ypos) = (location.x, location.y);
224 
225     // one cell after cursor position
226     let x = 0;
227     // one at row of cursor position
228     let y = 0;
229 
230     // location where to start clearing
231     let start_location = Coord::new(x, y);
232 
233     // get sum cells before cursor
234     let cells_to_write = (buffer_size.width as u32 * ypos as u32) + (xpos as u32 + 1);
235 
236     // clear everything before cursor position
237     clear_winapi(start_location, cells_to_write, current_attribute)
238 }
239 
clear_entire_screen(buffer_size: Size, current_attribute: u16) -> Result<()>240 fn clear_entire_screen(buffer_size: Size, current_attribute: u16) -> Result<()> {
241     // get sum cells before cursor
242     let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32;
243 
244     // location where to start clearing
245     let start_location = Coord::new(0, 0);
246 
247     // clear the entire screen
248     clear_winapi(start_location, cells_to_write, current_attribute)?;
249 
250     // put the cursor back at cell 0,0
251     cursor::sys::move_to(0, 0)?;
252     Ok(())
253 }
254 
clear_current_line(location: Coord, buffer_size: Size, current_attribute: u16) -> Result<()>255 fn clear_current_line(location: Coord, buffer_size: Size, current_attribute: u16) -> Result<()> {
256     // location where to start clearing
257     let start_location = Coord::new(0, location.y);
258 
259     // get sum cells before cursor
260     let cells_to_write = buffer_size.width as u32;
261 
262     // clear the whole current line
263     clear_winapi(start_location, cells_to_write, current_attribute)?;
264 
265     // put the cursor back at cell 1 on current row
266     cursor::sys::move_to(0, location.y as u16)?;
267     Ok(())
268 }
269 
clear_until_line(location: Coord, buffer_size: Size, current_attribute: u16) -> Result<()>270 fn clear_until_line(location: Coord, buffer_size: Size, current_attribute: u16) -> Result<()> {
271     let (x, y) = (location.x, location.y);
272 
273     // location where to start clearing
274     let start_location = Coord::new(x, y);
275 
276     // get sum cells before cursor
277     let cells_to_write = (buffer_size.width - x as i16) as u32;
278 
279     // clear until the current line
280     clear_winapi(start_location, cells_to_write, current_attribute)?;
281 
282     // put the cursor back at original cursor position before we did the clearing
283     cursor::sys::move_to(x as u16, y as u16)?;
284     Ok(())
285 }
286 
clear_winapi(start_location: Coord, cells_to_write: u32, current_attribute: u16) -> Result<()>287 fn clear_winapi(start_location: Coord, cells_to_write: u32, current_attribute: u16) -> Result<()> {
288     let console = Console::from(Handle::current_out_handle()?);
289     console.fill_whit_character(start_location, cells_to_write, ' ')?;
290     console.fill_whit_attribute(start_location, cells_to_write, current_attribute)?;
291     Ok(())
292 }
293 
294 #[cfg(test)]
295 mod tests {
296     use std::{ffi::OsString, os::windows::ffi::OsStringExt};
297 
298     use winapi::um::wincon::GetConsoleTitleW;
299 
300     use crossterm_winapi::ScreenBuffer;
301 
302     use super::{scroll_down, scroll_up, set_size, set_window_title, size};
303 
304     #[test]
test_resize_winapi()305     fn test_resize_winapi() {
306         let (width, height) = size().unwrap();
307 
308         set_size(30, 30).unwrap();
309         assert_eq!((30, 30), size().unwrap());
310 
311         // reset to previous size
312         set_size(width, height).unwrap();
313         assert_eq!((width, height), size().unwrap());
314     }
315 
316     // Test is disabled, because it's failing on Travis CI
317     #[test]
318     #[ignore]
test_scroll_down_winapi()319     fn test_scroll_down_winapi() {
320         let current_window = ScreenBuffer::current()
321             .unwrap()
322             .info()
323             .unwrap()
324             .terminal_window();
325 
326         scroll_down(2).unwrap();
327 
328         let new_window = ScreenBuffer::current()
329             .unwrap()
330             .info()
331             .unwrap()
332             .terminal_window();
333 
334         assert_eq!(new_window.top, current_window.top + 2);
335         assert_eq!(new_window.bottom, current_window.bottom + 2);
336     }
337 
338     // Test is disabled, because it's failing on Travis CI
339     #[test]
340     #[ignore]
test_scroll_up_winapi()341     fn test_scroll_up_winapi() {
342         // move the terminal buffer down before moving it up
343         test_scroll_down_winapi();
344 
345         let current_window = ScreenBuffer::current()
346             .unwrap()
347             .info()
348             .unwrap()
349             .terminal_window();
350 
351         scroll_up(2).unwrap();
352 
353         let new_window = ScreenBuffer::current()
354             .unwrap()
355             .info()
356             .unwrap()
357             .terminal_window();
358 
359         assert_eq!(new_window.top, current_window.top - 2);
360         assert_eq!(new_window.bottom, current_window.bottom - 2);
361     }
362 
363     #[test]
test_set_title_winapi()364     fn test_set_title_winapi() {
365         let test_title = "this is a crossterm test title";
366         set_window_title(test_title).unwrap();
367 
368         let mut raw = [0 as u16; 128];
369         let length = unsafe { GetConsoleTitleW(raw.as_mut_ptr(), raw.len() as u32) } as usize;
370         assert_ne!(0, length);
371 
372         let console_title = OsString::from_wide(&raw[..length]).into_string().unwrap();
373         assert_eq!(test_title, &console_title[..]);
374     }
375 }
376