Rendering buffer in stdout using crossterm

pub fn draw(&self) -> anyhow::Result<()> {
    let mut stdout = self.stdout.borrow_mut();
    let mut lines = self.text.lines_at(self.y_offset);

    eprintln!("first line: {:#?}", lines.clone().next());
    eprintln!("size.1: {}", self.size.1);
    
    for i in 0..self.size.1 - 1 {
        let line = lines.next().unwrap_or("".into());
        let str = format!("{line:<width$}", width = self.size.0 as usize);

        if i < 3 {
            eprintln!("Line {}: len={}, content={:?}", i, str.len(), str);
        }

        stdout.queue(cursor::MoveTo(0, i))?.queue(Print(str))?;
    }

    stdout
        .queue(cursor::MoveTo(0, 40))?
        .queue(Print("AAAAAAAAAAAAAAAAAAA"))?;

    let cursor = self.cursor.head();
    stdout.queue(cursor::MoveTo(cursor.0, cursor.1))?;
    stdout.flush()?;

    Ok(())
}

Here's snippet of function used to render a visible part of buffer into stdout.
I'm redirecting stderr to another tty so i'm using eprintln!.
I'm using ropey::Rope as buffer

line and self.y_offset are always correct. Checked them several times.
lines in if i < 3 are correct to render.
Incrementing self.y_offset is working as well.
That's the only function that interacts with stdout besides 1 stdout.flush() in the caller function. (I'm not even sure if it is needed there)

height of terminal (`self.size.1') is 41, but actual visible height is 40.

I have 2 questions:

  • Why i can't use for i in 0..self.size.1? If i use it, lines below will be copied on the line above with each rerender even if i don't change self.y_offset. So after 40 rerenders the bottom line will overlap the first one. That also means i cant use 40th line to render buffer there.
  • Why if i use for i in 0..self.size.1 - 1 it will draw correct while i don't change self.y_offset, and when i change it will copy all lines to line aboves, but instead of doing so every rerender it actually only make it once.

My guess was i didn't overwrite previous buffer and those lines were overwriting new ones if new ones had less length, but that's not the case because i create padding equal to terminal width. And rendering each line in a loop means new line will overwrite the padding if it was too big.

Whenever you write a character to the rightmost column of the terminal, the cursor position wraps — moves to the beginning of the next line. If you do this on the last line, the terminal scrolls up to create a new empty line for the cursor to be on.

DisableLineWrap may help you avoid this. Don’t forget to undo it when your program exits.

That didn't work for me.
I'm using one of my files as content for buffer. So after i run program i get this output in stdout:

use std::{
    io::{Write, stdout},
    path::Path,
};

use anyhow::Result;
use crossterm::{
    ExecutableCommand, QueueableCommand,
    event::{self, Event, KeyCode},
    terminal::{self, Clear, ClearType, DisableLineWrap, EnableLineWrap, EnterAlterna};Screen},

use crate::{action::Action, buffer::Buffer};

pub struct Editor {
    buffer: Buffer,
}

impl Editor {
    pub fn new() -> Self {
        Editor {
            buffer: Buffer::new(),
        }
    }

    pub fn from_file<T: AsRef<Path>>(path: T) -> Self {
        Editor {
            buffer: Buffer::from_file(path),
        }
    }

    fn main_loop(mut self) -> Result<()> {
        loop {
            self.draw()?;
            let ev = event::read()?;
            let action = self.handle_event(ev);
            if action.execute(&mut self.buffer) {
                break;
            }

AAAAAAAAAAAAAAAAAAA

self.y_offset changes only when cursor reaches bottom and tries to go lower. In that case on line AAAAAAAAAAAAAAAAAAA (without that line the output is the same)

when i'm trying to change self.y_offset my output is:

    io::{Write, stdout},
    path::Path, stdout},
};  path::Path,
};
use anyhow::Result;
use crossterm::{lt;
    ExecutableCommand, QueueableCommand,
    event::{self, Event, KeyCode},mmand,
    terminal::{self, Clear, ClearType, DisableLineWrap, EnableLineWrap, EnterAlterna};Screen},al::{self, Clear, ClearType, DisableLineWrap, EnableLineWrap, EnterAlterna};Screen},
use crate::{action::Action, buffer::Buffer};
use crate::{action::Action, buffer::Buffer};
pub struct Editor {
    buffer: Buffer,
}   buffer: Buffer,
}
impl Editor {
    pub fn new() -> Self {
        Editor { -> Self {
            buffer: Buffer::new(),
        }   buffer: Buffer::new(),
    }   }
    }
    pub fn from_file<T: AsRef<Path>>(path: T) -> Self {
        Editor {file<T: AsRef<Path>>(path: T) -> Self {
            buffer: Buffer::from_file(path),
        }   buffer: Buffer::from_file(path),
    }   }
    }
    fn main_loop(mut self) -> Result<()> {
        loop {op(mut self) -> Result<()> {
            self.draw()?;
            let ev = event::read()?;
            let action = self.handle_event(ev);
            if action.execute(&mut self.buffer) {
                break;execute(&mut self.buffer) {
            }   break;
            }
            // let (_, y) = self.buffer.cursor.head();

And they continue overlap with each other on every redraw

i played around a bit. Here is simplified version of code:

use std::io::{Write, stdout};

use anyhow::Result;
use crossterm::{
    QueueableCommand, cursor,
    event::{self, KeyCode},
    style::Print,
    terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
};
use ropey::Rope;

fn main() -> anyhow::Result<()> {
    let mut offset = 0;

    let mut stdout = stdout();
    stdout
        .queue(EnterAlternateScreen)?
        .queue(Clear(ClearType::All))?;

    terminal::enable_raw_mode()?;

    let path = "../pracs/src/main.rs";
    let buffer = Rope::from(std::fs::read_to_string(path).unwrap());

    draw(&buffer, offset)?;
    loop {
        if handle_event() {
            break;
        } else {
            offset += 1;
        }
        draw(&buffer, offset)?;
    }

    terminal::disable_raw_mode()?;
    stdout.queue(LeaveAlternateScreen)?;

    Ok(())
}

pub fn draw(buffer: &Rope, offset: usize) -> Result<()> {
    let mut stdout = stdout();
    let (x, y) = terminal::size()?;
    let mut lines = buffer.lines_at(offset);

    for i in 0..y {
        let line = lines.next().unwrap_or("".into());
        stdout
            .queue(cursor::MoveTo(0, i))?
            .queue(Print(format!("{}", " ".repeat(x as usize))))?
            .queue(cursor::MoveTo(0, i))?
            .queue(Print(format!("{line}",)))?;

        // let line = format!(
        //     "{line}{}",
        //     " ".repeat(x.saturating_sub(line.len_chars() as u16) as usize)
        // );

        // eprintln!("Line: {i}, len: {} content: {line:#?}", line.len());
        // stdout
        //     .queue(cursor::MoveTo(0, i))?
        //     .queue(Print(line))?;
        stdout.flush()?;
    }

    Ok(())
}

pub fn handle_event() -> bool {
    let ev = event::read().unwrap();
    match ev {
        event::Event::Key(c) => match c.code {
            KeyCode::Esc => return true,
            _ => {}
        },
        _ => {}
    }
    false
}

For whatever reason this works. But now I'm manually overwriting every line with spaces. This works. But same thing, formated line with spaces (commented snippet), doesn't work. Any thoughts why this happens?

Upd: I've found the problem. I'm using rope and taking lines by iterating over them. They still have \n at the end and this breaks space padding.
I do need \n's i guess. I want to move cursor to beginning of the next line if cursor goes after \n, and i want this behavior to be configurable.

Probably partial updating won't work here because all lines must be moved.
And i'm not really sure stdout.queue(Clear(ClearType::All)) is good to use in this case