1use std::io::Write as _;
8
9use snafu::{OptionExt as _, ResultExt as _};
10use termwiz::surface::Change as TermwizChange;
11use termwiz::surface::Position as TermwizPosition;
12
13#[inline]
20pub fn raw_string_direct_to_terminal(
21 string: &str,
22) -> Result<(), crate::errors::ShadowTerminalError> {
23 std::io::stdout()
24 .write(string.as_bytes())
25 .with_whatever_context(|err| {
26 format!("Writing direct raw output to user's terminal: {err:?}")
27 })?;
28 std::io::stdout().flush().with_whatever_context(|err| {
29 format!("Writing direct raw output to user's terminal: {err:?}")
30 })
31}
32
33#[derive(
36 Clone, Debug, Default, serde::Serialize, serde::Deserialize, schemars::JsonSchema, Eq, PartialEq,
37)]
38#[non_exhaustive]
39pub enum ScreenMode {
40 #[default]
43 Primary,
44 Alternate,
46}
47
48#[derive(Clone)]
50#[non_exhaustive]
51pub enum SurfaceDiff {
52 Scrollback(ScrollbackDiff),
54 Screen(ScreenDiff),
57}
58
59#[derive(Clone, Debug, Default)]
65#[non_exhaustive]
66pub struct ScrollbackDiff {
67 pub changes: Vec<TermwizChange>,
70 pub size: (usize, usize),
72 pub position: usize,
74 pub height: usize,
76}
77
78#[derive(Clone, Debug, Default)]
85#[non_exhaustive]
86pub struct ScreenDiff {
87 pub changes: Vec<TermwizChange>,
90 pub mode: ScreenMode,
92 pub size: (usize, usize),
94 pub cursor: wezterm_term::CursorPosition,
96}
97
98impl std::fmt::Debug for SurfaceDiff {
99 #[expect(clippy::min_ident_chars, reason = "It's in the standard library")]
100 #[inline]
101 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102 let info = match self {
103 Self::Scrollback(diff) => ("Scrollback", diff.changes.len(), diff.size),
104 Self::Screen(diff) => ("Screen", diff.changes.len(), diff.size),
105 };
106 write!(
107 f,
108 "{} diff of {} change(s) {}x{}",
109 info.0, info.1, info.2 .0, info.2 .1,
110 )
111 }
112}
113
114#[derive(Clone)]
121#[non_exhaustive]
122pub enum CompleteSurface {
123 Scrollback(CompleteScrollback),
125 Screen(CompleteScreen),
128}
129
130impl std::fmt::Debug for CompleteSurface {
131 #[expect(clippy::min_ident_chars, reason = "It's in the standard library")]
132 #[inline]
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 let info = match self {
135 Self::Scrollback(scrollback) => ("scrollback", &scrollback.surface),
136 Self::Screen(screen) => ("screen", &screen.surface),
137 };
138 write!(
139 f,
140 "Complete {} surface: {}x{}",
141 info.0,
142 info.1.dimensions().0,
143 info.1.dimensions().1
144 )
145 }
146}
147
148#[derive(Default, Clone)]
151#[non_exhaustive]
152pub struct CompleteScrollback {
153 pub surface: termwiz::surface::Surface,
155 pub position: usize,
157}
158
159#[derive(Default, Clone)]
161#[non_exhaustive]
162pub struct CompleteScreen {
163 pub surface: termwiz::surface::Surface,
165 pub mode: ScreenMode,
167}
168
169impl CompleteScreen {
170 #[inline]
172 #[must_use]
173 pub fn new(width: usize, height: usize) -> Self {
174 Self {
175 surface: termwiz::surface::Surface::new(width, height),
176 mode: ScreenMode::default(),
177 }
178 }
179}
180
181#[derive(Clone, Debug)]
182#[non_exhaustive]
183pub enum Output {
185 Diff(SurfaceDiff),
187 Complete(CompleteSurface),
190}
191
192#[derive(Debug)]
194#[non_exhaustive]
195pub enum SurfaceKind {
196 Scrollback,
198 Screen,
201}
202
203impl Default for SurfaceDiff {
204 #[inline]
205 fn default() -> Self {
206 Self::Scrollback(ScrollbackDiff::default())
207 }
208}
209
210impl crate::shadow_terminal::ShadowTerminal {
211 pub(crate) fn build_current_output(
213 &mut self,
214 kind: &SurfaceKind,
215 ) -> Result<Output, crate::errors::ShadowTerminalError> {
216 tracing::trace!("Converting Wezterm terminal state to a `termwiz::surface::Surface`");
217
218 let tty_size = self.terminal.get_size();
219 let total_lines = self.terminal.screen().scrollback_rows();
220 let changed_line_ids = self.terminal.screen().get_changed_stable_rows(
221 0..total_lines.try_into().with_whatever_context(|err| {
222 format!("Couldn't convert `total_lines` to `isize`: {err:?}")
223 })?,
224 self.last_sent.pty_sequence,
225 );
226
227 let is_diff_efficient = match kind {
229 SurfaceKind::Scrollback => changed_line_ids.len() < total_lines.div_euclid(2),
230 SurfaceKind::Screen => changed_line_ids.len() < tty_size.rows,
231 };
232
233 let is_building_screen = matches!(kind, SurfaceKind::Screen);
234 let is_resized = self.last_sent.pty_size != (tty_size.cols, tty_size.rows);
235 let is_diff_possible = !is_resized && !is_building_screen;
236
237 let output = if is_diff_efficient && is_diff_possible {
238 self.build_diff(kind, changed_line_ids, tty_size, total_lines)?
239 } else {
240 self.build_complete_surface(kind, tty_size, total_lines)?
241 };
242
243 Ok(output)
244 }
245
246 fn get_screen_mode(&self) -> ScreenMode {
248 if self.terminal.is_alt_screen_active() {
249 ScreenMode::Alternate
250 } else {
251 ScreenMode::Primary
252 }
253 }
254
255 fn build_diff(
257 &mut self,
258 kind: &SurfaceKind,
259 changed_line_ids: Vec<wezterm_term::StableRowIndex>,
260 tty_size: wezterm_term::TerminalSize,
261 total_lines: usize,
262 ) -> Result<Output, crate::errors::ShadowTerminalError> {
263 tracing::trace!("Building diff from Wezterm for {kind:?} from lines: {changed_line_ids:?}");
264
265 let changes = self.generate_changes(kind, Some(changed_line_ids))?;
266 let diff = match kind {
267 SurfaceKind::Scrollback => SurfaceDiff::Scrollback(ScrollbackDiff {
268 changes,
269 size: (tty_size.cols, tty_size.rows),
270 position: self.scroll_position,
271 height: total_lines,
272 }),
273 SurfaceKind::Screen => SurfaceDiff::Screen(ScreenDiff {
274 mode: self.get_screen_mode(),
275 changes,
276 size: (tty_size.cols, tty_size.rows),
277 cursor: self.terminal.cursor_pos(),
278 }),
279 };
280 Ok(Output::Diff(diff))
281 }
282
283 fn build_complete_surface(
285 &mut self,
286 kind: &SurfaceKind,
287 tty_size: wezterm_term::TerminalSize,
288 total_lines: usize,
289 ) -> Result<Output, crate::errors::ShadowTerminalError> {
290 tracing::trace!(
291 "Building surface or diff from Wezterm for {kind:?} from lines: 0 to {total_lines:?}"
292 );
293
294 let changes = self.generate_changes(kind, None)?;
295 let complete_surface = match kind {
296 SurfaceKind::Scrollback => {
297 let changes_count = changes.len();
298 let mut surface = termwiz::surface::Surface::new(tty_size.cols, total_lines);
299 surface.add_changes(changes);
300 tracing::trace!(
301 "Sending complete Scrollback ({} changes): Sample:\n{:.100}\n...",
302 changes_count,
303 surface.screen_chars_to_string()
304 );
305 CompleteSurface::Scrollback(CompleteScrollback {
306 surface,
307 position: self.scroll_position,
308 })
309 }
310 SurfaceKind::Screen => {
311 let changes_count = changes.len();
312 let mut surface = termwiz::surface::Surface::new(tty_size.cols, tty_size.rows);
313 surface.add_changes(changes);
314 tracing::trace!(
315 "Sending complete Screen ({}x{}, {} changes): Sample:\n{:.1000}\n...",
316 tty_size.cols,
317 tty_size.rows,
318 changes_count,
319 surface.screen_chars_to_string()
320 );
321 CompleteSurface::Screen(CompleteScreen {
322 surface,
323 mode: self.get_screen_mode(),
324 })
325 }
326 };
327
328 Ok(Output::Complete(complete_surface))
329 }
330
331 fn generate_changes(
334 &mut self,
335 kind: &SurfaceKind,
336 maybe_dirty_lines: Option<Vec<isize>>,
337 ) -> Result<Vec<TermwizChange>, crate::errors::ShadowTerminalError> {
338 let mut changes = Vec::new();
339 let (line_ids, output_start) = self.calculate_line_ids(kind, maybe_dirty_lines)?;
340 let screen = self.terminal.screen_mut();
341
342 for line_id in line_ids {
343 let line = screen.line_mut(line_id);
344 let y = line_id - output_start;
345 changes.push(TermwizChange::CursorPosition {
346 x: TermwizPosition::Absolute(0),
347 y: TermwizPosition::Absolute(y),
348 });
349
350 let mut wide_character_offset = 0;
351 for cell in line.cells_mut() {
352 if wide_character_offset > 0 {
359 wide_character_offset -= 1;
360 continue;
361 }
362
363 let mut attributes = vec![
364 TermwizChange::AllAttributes(cell.attrs().clone()),
365 cell.str().into(),
366 ];
367 wide_character_offset = cell.width() - 1;
368
369 changes.append(&mut attributes);
370 }
371 }
372
373 self.cursor_state(&mut changes)?;
374
375 Ok(changes)
376 }
377
378 fn cursor_state(
380 &self,
381 changes: &mut Vec<TermwizChange>,
382 ) -> Result<(), crate::errors::ShadowTerminalError> {
383 let cursor = self.terminal.cursor_pos();
384
385 let x = cursor.x;
386 let y = cursor.y.try_into().with_whatever_context(|err| {
387 format!("Couldn't convert cursor position to usize: {err:?}")
388 })?;
389 changes.push(TermwizChange::CursorPosition {
390 x: TermwizPosition::Absolute(x),
391 y: TermwizPosition::Absolute(y),
392 });
393
394 changes.push(TermwizChange::CursorShape(cursor.shape));
395 changes.push(TermwizChange::CursorVisibility(cursor.visibility));
396
397 Ok(())
398 }
399
400 fn calculate_line_ids(
403 &mut self,
404 kind: &SurfaceKind,
405 maybe_dirty_lines: Option<Vec<isize>>,
406 ) -> Result<(Vec<usize>, usize), crate::errors::ShadowTerminalError> {
407 let tty_size = self.terminal.get_size();
408 let screen = self.terminal.screen_mut();
409 let mut line_ids: Vec<usize> = Vec::new();
410 let (output_start, output_end) = match kind {
411 SurfaceKind::Scrollback => (0, screen.scrollback_rows()),
412 SurfaceKind::Screen => {
413 let end = screen.scrollback_rows() - self.scroll_position;
414 let start = end - tty_size.rows;
415 (start, end)
416 }
417 };
418
419 match maybe_dirty_lines {
420 Some(dirty_lines) => {
421 for stable_dirty_line in dirty_lines {
422 let physical_line_id = screen
423 .stable_row_to_phys(stable_dirty_line)
424 .with_whatever_context(|| {
425 "Couldn't get physical row ID from stable row ID"
426 })?;
427 line_ids.push(physical_line_id);
428 }
429 }
430 None => {
431 for line_id in output_start..output_end {
432 line_ids.push(line_id);
433 }
434 }
435 }
436
437 Ok((line_ids, output_start))
438 }
439}
440
441#[cfg(test)]
442mod test {
443 #[cfg(not(target_os = "windows"))]
444 #[tokio::test(flavor = "multi_thread")]
445 async fn wide_characters() {
446 let mut stepper = Box::pin(crate::tests::helpers::run(Some(100), None)).await;
447 let columns = stepper.shadow_terminal.terminal.get_size().cols;
448 let full_row = "😀".repeat(columns.div_euclid(2));
449
450 let command = format!("echo {full_row}");
451 stepper.send_command(command.as_str()).unwrap();
452
453 let raw_with_spaces = full_row
456 .chars()
457 .map(|character| character.to_string())
458 .collect::<Vec<String>>()
459 .join(" ");
460
461 stepper
462 .wait_for_string(&raw_with_spaces, None)
463 .await
464 .unwrap();
465 }
466}