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

That was very good clue to the answer, but i couldn't understand it back then.

As far as i know wrapping lanes are disabled by default in crossterm. At least in raw mode.

The problem is i rendered rope line by line. Rope uses \n symbol to separate it's content as lines.
When \n is rendered in terminal it forces cursor to be moved on the next line. It's okay till the last line because the last \n will push the cursor down and terminal will scroll down.

The solution to this would be trim the last \n at all lines and render it just like that. That's pretty strange that i can't change this behavior to still be able to render \n, but it is what it is.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.