use std::{
fmt,
sync::{Arc, Mutex, PoisonError},
};
use super::emulator::Emulator;
use super::prediction::{OverlayCell, OverlayCursor, PredictionEngine};
pub struct Renderer {
displayed: vt100::Parser,
initialized: bool,
}
impl fmt::Debug for Renderer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Renderer")
.field("initialized", &self.initialized)
.finish_non_exhaustive()
}
}
impl Renderer {
#[must_use]
pub fn new(rows: u16, cols: u16) -> Self {
Self {
displayed: vt100::Parser::new(rows, cols, 0),
initialized: false,
}
}
pub fn set_size(&mut self, rows: u16, cols: u16) {
self.displayed.screen_mut().set_size(rows, cols);
self.initialized = false;
}
#[must_use]
pub fn render(
&mut self,
screen: &vt100::Screen,
overlays: &[OverlayCell],
cursor: Option<OverlayCursor>,
) -> Vec<u8> {
let new_alt = screen.alternate_screen();
let old_alt = self.displayed.screen().alternate_screen();
let alt_changed = new_alt != old_alt;
let (rows, cols) = screen.size();
let mut frame = vt100::Parser::new(rows, cols, 0);
frame.process(&screen.contents_formatted());
if !overlays.is_empty() {
let mut paint: Vec<u8> = Vec::with_capacity(overlays.len() * 12);
for cell in overlays {
write_to_vec(
&mut paint,
format_args!("\x1b[{};{}H", cell.row + 1, cell.col + 1),
);
if cell.flagged {
paint.extend_from_slice(b"\x1b[4m");
}
let mut char_buf = [0u8; 4];
paint.extend_from_slice(cell.ch.encode_utf8(&mut char_buf).as_bytes());
if cell.flagged {
paint.extend_from_slice(b"\x1b[24m");
}
}
frame.process(&paint);
}
let (cur_row, cur_col) = if let Some(oc) = cursor {
(oc.row, oc.col)
} else {
screen.cursor_position()
};
let mut mv: Vec<u8> = Vec::with_capacity(12);
write_to_vec(
&mut mv,
format_args!("\x1b[{};{}H", cur_row + 1, cur_col + 1),
);
frame.process(&mv);
let mut out: Vec<u8> = Vec::with_capacity(4096);
if new_alt && !old_alt {
out.extend_from_slice(b"\x1b[?1049h");
} else if !new_alt && old_alt {
out.extend_from_slice(b"\x1b[?1049l");
}
if self.initialized && !alt_changed {
if !new_alt
&& let Some(n) =
detect_scroll_up(self.displayed.screen(), frame.screen(), rows, cols)
{
let mut scroll: Vec<u8> = Vec::with_capacity(8 + usize::from(n));
write_to_vec(&mut scroll, format_args!("\x1b[{rows};1H"));
scroll.resize(scroll.len() + usize::from(n), b'\n');
self.displayed.process(&scroll);
out.extend_from_slice(&scroll);
}
let diff = frame.screen().contents_diff(self.displayed.screen());
self.displayed.process(&diff);
out.extend_from_slice(&diff);
} else {
let full = frame.screen().contents_formatted();
self.displayed.process(&out);
self.displayed.process(&full);
out.extend_from_slice(&full);
self.initialized = true;
}
out
}
pub fn invalidate(&mut self) {
self.initialized = false;
}
}
#[must_use]
pub fn paint_overlays_to_ansi(overlays: &[OverlayCell], cursor: Option<OverlayCursor>) -> Vec<u8> {
if overlays.is_empty() && cursor.is_none() {
return Vec::new();
}
let mut out: Vec<u8> = Vec::with_capacity(256);
if !overlays.is_empty() {
out.extend_from_slice(b"\x1b[s"); for cell in overlays {
write_to_vec(
&mut out,
format_args!("\x1b[{};{}H", cell.row + 1, cell.col + 1),
);
if cell.flagged {
out.extend_from_slice(b"\x1b[4m");
}
let mut char_buf = [0u8; 4];
let s = cell.ch.encode_utf8(&mut char_buf);
out.extend_from_slice(s.as_bytes());
if cell.flagged {
out.extend_from_slice(b"\x1b[24m");
}
}
out.extend_from_slice(b"\x1b[m"); out.extend_from_slice(b"\x1b[u"); }
if let Some(oc) = cursor {
write_to_vec(
&mut out,
format_args!("\x1b[{};{}H", oc.row + 1, oc.col + 1),
);
}
out
}
#[must_use]
pub fn render_server_update(
emulator: &Arc<Mutex<Emulator>>,
prediction: &Arc<Mutex<PredictionEngine>>,
renderer: &Arc<Mutex<Renderer>>,
with_predictions: bool,
) -> Vec<u8> {
let emu = emulator.lock().unwrap_or_else(PoisonError::into_inner);
let screen = emu.screen();
let (overlays, cursor) = if with_predictions {
let mut pred = prediction.lock().unwrap_or_else(PoisonError::into_inner);
pred.cull(screen);
pred.apply(screen)
} else {
(Vec::new(), None)
};
let mut rend = renderer.lock().unwrap_or_else(PoisonError::into_inner);
rend.render(screen, &overlays, cursor)
}
#[must_use]
pub fn render_prediction_update(
emulator: &Arc<Mutex<Emulator>>,
prediction: &Arc<Mutex<PredictionEngine>>,
renderer: &Arc<Mutex<Renderer>>,
new_bytes: &[u8],
) -> Vec<u8> {
let emu = emulator.lock().unwrap_or_else(PoisonError::into_inner);
let screen = emu.screen();
let (overlays, cursor) = {
let mut pred = prediction.lock().unwrap_or_else(PoisonError::into_inner);
for &byte in new_bytes {
pred.new_user_byte(byte, screen);
}
pred.apply(screen)
};
let mut rend = renderer.lock().unwrap_or_else(PoisonError::into_inner);
rend.render(screen, &overlays, cursor)
}
fn write_to_vec(buf: &mut Vec<u8>, args: fmt::Arguments<'_>) {
use std::io::Write as _;
drop(buf.write_fmt(args));
}
fn detect_scroll_up(
prev: &vt100::Screen,
frame: &vt100::Screen,
rows: u16,
cols: u16,
) -> Option<u16> {
if rows < 2 {
return None;
}
let row_text = |s: &vt100::Screen, r: u16| s.contents_between(r, 0, r, cols);
let prev_rows: Vec<String> = (0..rows).map(|r| row_text(prev, r)).collect();
let frame_rows: Vec<String> = (0..rows).map(|r| row_text(frame, r)).collect();
let is_blank = |s: &str| s.trim_end().is_empty();
if prev_rows == frame_rows {
return None;
}
let shifted = |n: u16, len: u16| -> bool {
let n = usize::from(n);
let len = usize::from(len);
(0..len).all(|i| frame_rows[i] == prev_rows[i + n])
&& (0..len).any(|i| !is_blank(&frame_rows[i]))
};
for n in 1..rows {
if shifted(n, rows - n) {
return Some(n);
}
}
if is_blank(&frame_rows[usize::from(rows - 1)]) {
for n in 1..rows - 1 {
if shifted(n, rows - 1 - n) {
return Some(n);
}
}
}
None
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::{
Emulator, PredictionEngine, Renderer, detect_scroll_up, paint_overlays_to_ansi,
render_prediction_update, render_server_update,
};
struct Harness {
emu: Emulator,
renderer: Renderer,
term: vt100::Parser,
}
impl Harness {
fn new(rows: u16, cols: u16) -> Self {
Self {
emu: Emulator::new(rows, cols),
renderer: Renderer::new(rows, cols),
term: vt100::Parser::new(rows, cols, 1000),
}
}
fn feed(&mut self, bytes: &[u8]) {
self.emu.process(bytes);
let out = self.renderer.render(self.emu.screen(), &[], None);
self.term.process(&out);
}
fn scrollback_depth(&mut self) -> usize {
self.term.screen_mut().set_scrollback(usize::MAX);
let depth = self.term.screen().scrollback();
self.term.screen_mut().set_scrollback(0);
depth
}
fn term_row(&self, r: u16) -> String {
self.term.screen().contents_between(r, 0, r, 80)
}
}
#[test]
fn detect_scroll_up_recognizes_clean_shift() {
let mut prev = vt100::Parser::new(4, 80, 0);
prev.process(b"\x1b[1;1Haaa\r\nbbb\r\nccc\r\nddd");
let mut frame = vt100::Parser::new(4, 80, 0);
frame.process(b"\x1b[1;1Hccc\r\nddd\r\neee\r\nfff");
assert_eq!(
detect_scroll_up(prev.screen(), frame.screen(), 4, 80),
Some(2)
);
}
#[test]
fn detect_scroll_up_ignores_unchanged_and_blank() {
let mut a = vt100::Parser::new(4, 80, 0);
a.process(b"\x1b[1;1Haaa\r\nbbb\r\nccc\r\nddd");
let mut b = vt100::Parser::new(4, 80, 0);
b.process(b"\x1b[1;1Haaa\r\nbbb\r\nccc\r\nddd");
assert_eq!(detect_scroll_up(a.screen(), b.screen(), 4, 80), None);
let blank = vt100::Parser::new(4, 80, 0);
let mut one = vt100::Parser::new(4, 80, 0);
one.process(b"\x1b[4;1Hxxx");
assert_eq!(detect_scroll_up(blank.screen(), one.screen(), 4, 80), None);
}
#[test]
fn native_scroll_pushes_rows_into_local_scrollback() {
let mut h = Harness::new(24, 80);
for i in 0..24 {
h.feed(format!("line{i:02}\r\n").as_bytes());
}
let before = h.scrollback_depth();
for i in 24..40 {
h.feed(format!("line{i:02}\r\n").as_bytes());
}
let after = h.scrollback_depth();
assert!(
after > before,
"scrolled-off lines must enter local scrollback (before={before}, after={after})"
);
assert!(
(0..24).any(|r| h.term_row(r).starts_with("line39")),
"latest line must be on screen"
);
}
#[test]
fn scrolled_off_line_is_retrievable_from_local_scrollback() {
let mut h = Harness::new(24, 80);
for i in 0..40 {
h.feed(format!("line{i:02}\r\n").as_bytes());
}
h.term.screen_mut().set_scrollback(usize::MAX);
let reachable = (0..24).any(|r| h.term.screen().contents_between(r, 0, r, 80) == "line00");
h.term.screen_mut().set_scrollback(0);
assert!(
reachable,
"an early scrolled-off line must live in the terminal's scrollback"
);
}
#[test]
fn batched_multi_line_scroll_is_detected_in_one_render() {
let mut h = Harness::new(6, 80);
h.feed(b"\x1b[1;1Hr0\r\nr1\r\nr2\r\nr3\r\nr4\r\nr5");
let before = h.scrollback_depth();
h.feed(b"\r\nr6\r\nr7\r\nr8\r\n");
let after = h.scrollback_depth();
assert_eq!(after, before + 4, "a 4-row batch scroll must push 4 rows");
assert_eq!(h.term_row(4).trim_end(), "r8");
}
#[test]
fn single_linefeed_scroll_is_detected() {
let mut h = Harness::new(4, 80);
h.feed(b"\x1b[1;1Ha\r\nb\r\nc\r\nd");
let before = h.scrollback_depth();
h.feed(b"\r\ne");
let after = h.scrollback_depth();
assert_eq!(
after,
before + 1,
"a one-row scroll must push exactly one row"
);
assert_eq!(h.term_row(3).trim_end(), "e");
}
#[test]
fn apt_fancy_progress_does_not_scroll_local_scrollback() {
let mut h = Harness::new(24, 80);
h.feed(b"\x1b[2J\x1b[H\x1b[1;23r\x1b[1;1H");
for (i, pct) in [(1u32, "20%"), (2, "40%"), (3, "60%"), (4, "80%")] {
let frame = format!(
"Unpacking pkg{i:02}...\r\n\x1b7\x1b[24;1H\x1b[42m[bar] {pct}\x1b[0m\x1b[K\x1b8"
);
h.feed(frame.as_bytes());
}
assert_eq!(
h.scrollback_depth(),
0,
"a scroll-region progress bar must not spill into local scrollback"
);
assert!(
h.term_row(23).contains("80%"),
"the progress bar stays pinned to the last row"
);
}
#[test]
fn alt_screen_scroll_does_not_touch_scrollback() {
let mut h = Harness::new(24, 80);
h.feed(b"\x1b[?1049h\x1b[1;1H");
for i in 0..40 {
h.feed(format!("alt{i:02}\r\n").as_bytes());
}
assert_eq!(
h.scrollback_depth(),
0,
"the alternate screen has no scrollback; never emit a real scroll there"
);
}
#[test]
fn set_size_resets_renderer_and_updates_dimensions() {
let mut renderer = Renderer::new(24, 80);
let mut parser = vt100::Parser::new(24, 80, 0);
parser.process(b"hello");
drop(renderer.render(parser.screen(), &[], None));
assert!(renderer.initialized);
renderer.set_size(30, 100);
assert!(!renderer.initialized);
}
#[test]
fn renderer_new_is_not_initialized() {
let r = Renderer::new(24, 80);
assert!(!r.initialized);
}
#[test]
fn renderer_first_render_sets_initialized() {
let mut r = Renderer::new(24, 80);
let parser = vt100::Parser::new(24, 80, 0);
let out = r.render(parser.screen(), &[], None);
assert!(r.initialized);
assert!(!out.is_empty());
}
#[test]
fn renderer_invalidate_clears_initialized() {
let mut r = Renderer::new(24, 80);
let parser = vt100::Parser::new(24, 80, 0);
drop(r.render(parser.screen(), &[], None));
assert!(r.initialized);
r.invalidate();
assert!(!r.initialized);
}
#[test]
fn paint_overlays_to_ansi_empty_returns_empty() {
let out = paint_overlays_to_ansi(&[], None);
assert!(out.is_empty());
}
#[test]
fn paint_overlays_to_ansi_with_cell_contains_escape_sequences() {
use super::super::prediction::OverlayCell;
let cells = vec![OverlayCell {
row: 0,
col: 0,
ch: 'a',
flagged: false,
}];
let out = paint_overlays_to_ansi(&cells, None);
let s = String::from_utf8_lossy(&out);
assert!(s.contains("\x1b[s"));
assert!(s.contains("\x1b[1;1H"));
assert!(s.contains('a'));
assert!(s.contains("\x1b[u"));
}
#[test]
fn render_with_content_produces_nonempty_output() {
let mut r = Renderer::new(24, 80);
let mut parser = vt100::Parser::new(24, 80, 0);
parser.process(b"hello");
let out = r.render(parser.screen(), &[], None);
assert!(!out.is_empty());
let s = String::from_utf8_lossy(&out);
assert!(s.contains("hello"));
}
#[test]
fn render_second_call_with_no_change_produces_minimal_output() {
let mut r = Renderer::new(24, 80);
let mut parser = vt100::Parser::new(24, 80, 0);
parser.process(b"hello");
let first = r.render(parser.screen(), &[], None);
assert!(!first.is_empty());
let second = r.render(parser.screen(), &[], None);
assert!(
second.len() < first.len(),
"second render with no changes should be smaller"
);
}
fn apply(term: &mut vt100::Parser, bytes: &[u8]) {
term.process(bytes);
}
#[test]
fn render_with_overlay_cell_paints_overlay_character() {
use super::super::prediction::OverlayCell;
let mut r = Renderer::new(24, 80);
let mut term = vt100::Parser::new(24, 80, 0);
let parser = vt100::Parser::new(24, 80, 0);
let overlays = vec![OverlayCell {
row: 0,
col: 0,
ch: 'Z',
flagged: false,
}];
apply(&mut term, &r.render(parser.screen(), &overlays, None));
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("Z"),
"overlay character 'Z' must be painted at (0,0)"
);
}
#[test]
fn render_with_flagged_overlay_underlines_the_cell() {
use super::super::prediction::OverlayCell;
let mut r = Renderer::new(24, 80);
let mut term = vt100::Parser::new(24, 80, 0);
let parser = vt100::Parser::new(24, 80, 0);
let overlays = vec![OverlayCell {
row: 2,
col: 5,
ch: 'F',
flagged: true,
}];
apply(&mut term, &r.render(parser.screen(), &overlays, None));
let cell = term.screen().cell(2, 5).expect("cell exists");
assert_eq!(cell.contents(), "F");
assert!(cell.underline(), "flagged overlay cell must be underlined");
}
#[test]
fn render_with_cursor_override_positions_cursor_at_override() {
use super::super::prediction::OverlayCursor;
let mut r = Renderer::new(24, 80);
let mut term = vt100::Parser::new(24, 80, 0);
let parser = vt100::Parser::new(24, 80, 0);
let cursor_override = Some(OverlayCursor { row: 5, col: 10 });
apply(&mut term, &r.render(parser.screen(), &[], cursor_override));
assert_eq!(
term.screen().cursor_position(),
(5, 10),
"cursor override must place the cursor at (5,10)"
);
}
#[test]
fn render_culled_prediction_self_heals() {
use super::super::prediction::OverlayCell;
let mut server = vt100::Parser::new(24, 80, 0);
server.process(b"a");
let mut r = Renderer::new(24, 80);
let mut term = vt100::Parser::new(24, 80, 0);
let overlay = vec![OverlayCell {
row: 0,
col: 0,
ch: 'X',
flagged: false,
}];
apply(&mut term, &r.render(server.screen(), &overlay, None));
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("X"),
"prediction must be visible in frame 1"
);
apply(&mut term, &r.render(server.screen(), &[], None));
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("a"),
"culled prediction must self-heal back to the real cell"
);
}
#[test]
fn render_no_sgr_bleed_after_flagged_overlay() {
use super::super::prediction::OverlayCell;
let mut server = vt100::Parser::new(24, 80, 0);
server.process(b"hi");
let mut r = Renderer::new(24, 80);
let mut term = vt100::Parser::new(24, 80, 0);
let overlay = vec![OverlayCell {
row: 0,
col: 5,
ch: 'P',
flagged: true,
}];
apply(&mut term, &r.render(server.screen(), &overlay, None));
assert!(
!term.screen().cell(0, 0).expect("cell").underline(),
"plain server cell must not inherit the overlay's underline"
);
assert!(
term.screen().cell(0, 5).expect("cell").underline(),
"flagged overlay cell should be underlined"
);
}
#[test]
fn paint_overlays_to_ansi_with_cursor_positions_cursor() {
use super::super::prediction::OverlayCursor;
let out = paint_overlays_to_ansi(&[], Some(OverlayCursor { row: 3, col: 7 }));
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("\x1b[4;8H"),
"cursor overlay must produce ESC[4;8H: {s:?}"
);
}
#[test]
fn render_emits_alt_screen_enter_on_transition() {
let mut r = Renderer::new(24, 80);
let mut p1 = vt100::Parser::new(24, 80, 0);
p1.process(b"hello");
drop(r.render(p1.screen(), &[], None));
let mut p2 = vt100::Parser::new(24, 80, 0);
p2.process(b"\x1b[?1049h");
let out = r.render(p2.screen(), &[], None);
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("\x1b[?1049h"),
"alt-screen enter must appear in output when transitioning to alt-screen: {s:?}"
);
}
#[test]
fn render_emits_alt_screen_exit_on_transition() {
let mut r = Renderer::new(24, 80);
let mut p1 = vt100::Parser::new(24, 80, 0);
p1.process(b"\x1b[?1049h");
drop(r.render(p1.screen(), &[], None));
let mut p2 = vt100::Parser::new(24, 80, 0);
p2.process(b"\x1b[?1049h\x1b[?1049l");
let out = r.render(p2.screen(), &[], None);
let s = String::from_utf8_lossy(&out);
assert!(
s.contains("\x1b[?1049l"),
"alt-screen exit must appear in output when transitioning to main screen: {s:?}"
);
}
#[test]
fn render_size_change_triggers_full_refresh() {
let mut r = Renderer::new(24, 80);
let mut parser = vt100::Parser::new(24, 80, 0);
parser.process(b"hello");
let first = r.render(parser.screen(), &[], None);
assert!(!first.is_empty());
r.set_size(30, 100);
assert!(!r.initialized);
let mut parser2 = vt100::Parser::new(30, 100, 0);
parser2.process(b"world");
let after_resize = r.render(parser2.screen(), &[], None);
assert!(r.initialized);
assert!(!after_resize.is_empty());
}
use super::super::prediction::DisplayPreference;
type RenderTrio = (
Arc<Mutex<Emulator>>,
Arc<Mutex<PredictionEngine>>,
Arc<Mutex<Renderer>>,
);
fn render_trio() -> RenderTrio {
(
Arc::new(Mutex::new(Emulator::new(24, 80))),
Arc::new(Mutex::new(PredictionEngine::new(DisplayPreference::Always))),
Arc::new(Mutex::new(Renderer::new(24, 80))),
)
}
#[test]
fn render_server_update_paints_emulator_screen() {
let (emulator, prediction, renderer) = render_trio();
emulator.lock().unwrap().process(b"server text");
let mut term = vt100::Parser::new(24, 80, 0);
apply(
&mut term,
&render_server_update(&emulator, &prediction, &renderer, true),
);
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("s"),
"server screen content must be rendered to the terminal"
);
}
#[test]
fn render_server_update_culls_stale_predictions() {
let (emulator, prediction, renderer) = render_trio();
emulator.lock().unwrap().process(b"a");
{
let emu = emulator.lock().unwrap();
let mut pred = prediction.lock().unwrap();
pred.new_user_byte(b'Z', emu.screen());
}
let mut term = vt100::Parser::new(24, 80, 0);
apply(
&mut term,
&render_server_update(&emulator, &prediction, &renderer, true),
);
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("a"),
"a prediction the server screen contradicts must be culled, leaving the real cell"
);
}
#[test]
fn render_server_update_without_predictions_skips_overlays() {
let (emulator, prediction, renderer) = render_trio();
emulator.lock().unwrap().process(b"hello");
{
let emu = emulator.lock().unwrap();
let mut pred = prediction.lock().unwrap();
pred.new_user_byte(b'X', emu.screen());
}
let mut term = vt100::Parser::new(24, 80, 0);
apply(
&mut term,
&render_server_update(&emulator, &prediction, &renderer, false),
);
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("h"),
"with_predictions=false must render only the raw server screen"
);
}
#[test]
fn render_prediction_update_paints_keystroke_once_confirmed() {
let (emulator, prediction, renderer) = render_trio();
{
let emu = emulator.lock().unwrap();
prediction.lock().unwrap().cull(emu.screen());
}
let mut term = vt100::Parser::new(24, 80, 0);
apply(
&mut term,
&render_prediction_update(&emulator, &prediction, &renderer, b"k"),
);
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some(""),
"a fresh (tentative) prediction must not be painted yet"
);
emulator.lock().unwrap().process(b"\x1b[1;2H");
apply(
&mut term,
&render_server_update(&emulator, &prediction, &renderer, true),
);
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("k"),
"once the epoch is confirmed the predicted keystroke must be painted"
);
}
#[test]
fn render_prediction_update_does_not_cull_between_keystrokes() {
let (emulator, prediction, renderer) = render_trio();
{
let emu = emulator.lock().unwrap();
prediction.lock().unwrap().cull(emu.screen());
}
let mut term = vt100::Parser::new(24, 80, 0);
apply(
&mut term,
&render_prediction_update(&emulator, &prediction, &renderer, b"a"),
);
apply(
&mut term,
&render_prediction_update(&emulator, &prediction, &renderer, b"b"),
);
emulator.lock().unwrap().process(b"\x1b[1;3H");
apply(
&mut term,
&render_server_update(&emulator, &prediction, &renderer, true),
);
assert_eq!(
term.screen().cell(0, 0).map(vt100::Cell::contents),
Some("a"),
"first prediction must survive the second keystroke"
);
assert_eq!(
term.screen().cell(0, 1).map(vt100::Cell::contents),
Some("b"),
"second prediction must be appended alongside the first"
);
}
}