Repository: osa1/tiny (language: rust)

Issue:
TUI feature request: grow text field vertically instead of scrolling
Currently if I write a message long enough to not fit on the screen I have to scroll back and forth to see rest of the message. Another idea is to vertically grow the text field instead. That way I can see the whole message on screen (assuming there's vertical space on screen).
Allow naming servers to give them a better display name
Currently the address of the server is used to display the server tab. For some servers this can be unhelpful (ip address) or too long (very long domain name). I would like to be able to give my servers a helpful name in the config. This name would be used instead.


Discussion / hints:
#125 suggests a similar thing for the tab line.
Is this being worked on? I might try to take a stab at it if it's still wanted/needed.
@trevarj I still need it but have no time to work in it these days, so go for it. Feel free to ping me here or on IRC with questions.


Fix patch:
diff --git a/libtiny_tui/src/input_area/input_line.rs b/libtiny_tui/src/input_area/input_line.rs
new file mode 100644
index 00000000..55a55d61
--- /dev/null
+++ b/libtiny_tui/src/input_area/input_line.rs
@@ -0,0 +1,431 @@
+use crate::{config::Colors, termbox, utils};
+use std::{cmp::min, ops::RangeBounds, vec::Drain};
+use termbox_simple::Termbox;
+
+#[derive(Clone, Debug)]
+pub(crate) struct InputLine {
+    /// Text that is being input
+    buffer: Vec<char>,
+    /// Vector of indexes of `buffer` where new lines start
+    /// Used when there is enough text in the buffer for wrapping
+    /// Gets invalidated/emptied when calculate_height is called
+    line_starts: Vec<i32>,
+}
+
+impl InputLine {
+    pub(crate) fn new() -> InputLine {
+        InputLine {
+            buffer: Vec::with_capacity(512),
+            line_starts: Vec::with_capacity(32),
+        }
+    }
+
+    /// Creates an InputLine from a buffer and generates word_splits
+    pub(crate) fn from_buffer(buffer: Vec<char>) -> InputLine {
+        InputLine {
+            buffer,
+            line_starts: Vec::with_capacity(32),
+        }
+    }
+
+    /// Returns pointer to InputLine's buffer
+    pub(crate) fn get_buffer(&self) -> &Vec<char> {
+        &self.buffer
+    }
+
+    /**
+     **    Functions to interface with InputLine::buffer
+     **/
+
+    /// Interface for Vec::get()
+    pub(crate) fn get(&self, idx: usize) -> char {
+        self.buffer[idx]
+    }
+
+    /// Interface for Vec::len()
+    pub(crate) fn len(&self) -> usize {
+        self.buffer.len()
+    }
+
+    /// Interface for Vec::drain()
+    pub(crate) fn drain<R>(&mut self, range: R) -> Drain<char>
+    where
+        R: RangeBounds<usize>,
+    {
+        self.buffer.drain(range)
+    }
+
+    /// Interface for Vec::remove()
+    pub(crate) fn remove(&mut self, idx: usize) -> char {
+        let removed = self.buffer.remove(idx);
+        removed
+    }
+
+    /// Interface for Vec::insert()
+    pub(crate) fn insert(&mut self, idx: usize, element: char) {
+        self.buffer.insert(idx, element);
+    }
+
+    /// Calculate how many lines of text will be in the textfield
+    /// based on the width of the widget, taking nickname length into account
+    /// Only needed when buffer is wider than width and scrolling is off
+    pub(crate) fn calculate_height(&mut self, width: i32, nick_length: usize) -> usize {
+        let mut line_count: i32 = 1;
+        let mut line_width: i32 = width - nick_length as i32;
+        let mut last_whitespace_idx: Option<i32> = None;
+        let mut prev_char_is_whitespace = false;
+        let mut current_idx = 0;
+        let mut current_line_length = 0;
+        // clear cached indexes
+        self.line_starts.clear();
+        for c in &self.buffer {
+            current_line_length += 1;
+            if c.is_whitespace() {
+                // Splitting
+                if current_line_length > line_width {
+                    // we're on a whitespace so just go to next line
+                    line_count += 1;
+                    // this character will be the first one on the next line
+                    current_line_length = 1;
+                    // set width to full width
+                    line_width = width;
+                    // store index for drawing
+                    self.line_starts.push(current_idx);
+                }
+                // store whitespace for splitting
+                last_whitespace_idx = Some(current_idx);
+                prev_char_is_whitespace = true;
+            } else {
+                // Splitting
+                if current_line_length > line_width {
+                    // if the previous character was a whitespace, then we had a clean split
+                    if !prev_char_is_whitespace && last_whitespace_idx.is_some() {
+                        // move back to the last whitespace and get the difference of characters that will be on the next line
+                        current_line_length = current_idx - last_whitespace_idx.unwrap();
+                        // store index for drawing
+                        self.line_starts.push(last_whitespace_idx.unwrap() + 1);
+                    } else {
+                        // unclean split on non-whitespace
+                        current_line_length = 1;
+                        // store index for drawing
+                        self.line_starts.push(current_idx);
+                    }
+                    // invalidate whitespace since we split here
+                    last_whitespace_idx = None;
+                    // moved to next line
+                    line_count += 1;
+                    // set width to full width
+                    line_width = width;
+                }
+                prev_char_is_whitespace = false;
+            }
+            current_idx += 1;
+        }
+
+        // ending line length is width, make room for cursor
+        if current_line_length == line_width {
+            line_count += 1;
+        }
+
+        line_count as usize
+    }
+}
+
+fn draw_line_scroll(
+    tb: &mut Termbox,
+    colors: &Colors,
+    line: &[char],
+    pos_x: i32,
+    pos_y: i32,
+    width: i32,
+    cursor: i32,
+    scroll: i32,
+) {
+    let slice: &[char] =
+        &line[scroll as usize..min(line.len(), (scroll + (width - pos_x)) as usize)];
+    termbox::print_chars(tb, pos_x, pos_y, colors.user_msg, slice.iter().cloned());
+    // On my terminal the cursor is only shown when there's a character
+    // under it.
+    if cursor as usize >= line.len() {
+        tb.change_cell(
+            pos_x + cursor - scroll,
+            pos_y,
+            ' ',
+            colors.cursor.fg,
+            colors.cursor.bg,
+        );
+    }
+
+    tb.set_cursor(Some(((pos_x + cursor - scroll) as u16, pos_y as u16)));
+}
+
+pub(crate) struct CompletionRange {
+    start_idx: usize,
+    end_idx: usize,
+}
+
+fn draw_line_wrapped(
+    tb: &mut Termbox,
+    colors: &Colors,
+    pos_x: i32,
+    pos_y: i32,
+    width: i32,
+    cursor: i32,
+    line: &InputLine,
+    completion_range: &Option<CompletionRange>,
+) {
+    let mut col = pos_x;
+    let mut line_num = 0;
+
+    let mut cursor_xychar = (0, 0, ' ');
+    let mut check_cursor = |char_idx: usize, cursor: i32, pos_x: i32, pos_y: i32, c: char| {
+        if char_idx == cursor as usize {
+            cursor_xychar = (pos_x, pos_y, c);
+        }
+    };
+    let mut char_idx: usize = 0;
+    let mut line_starts_iter = line.line_starts.iter().peekable();
+    for c in line.buffer.iter() {
+        let mut style = colors.user_msg;
+        // for autocompletion highlighting
+        if let Some(completion_range) = completion_range {
+            if char_idx >= completion_range.start_idx && char_idx < completion_range.end_idx {
+                style = colors.completion;
+            }
+        }
+        // If next_line_start is Some we already know the indexes for the start of each line
+        // If it is None then we just continue outputting on this line
+        let next_line_start = line_starts_iter.peek();
+        if let Some(next_line_start) = next_line_start {
+            if char_idx == **next_line_start as usize {
+                // move to next line
+                line_num += 1;
+                // reset column
+                col = 0;
+                // move to the next line start index
+                line_starts_iter.next();
+            }
+        }
+        // Write out the character
+        tb.change_cell(col, pos_y + line_num, *c, style.fg, style.bg);
+        // Check if the cursor is on this character
+        check_cursor(char_idx, cursor, col, pos_y + line_num, *c);
+        col += 1;
+        char_idx += 1;
+    }
+
+    // Cursor may be (probably) after all text
+    if col == width {
+        // render cursor on next line
+        line_num += 1;
+        col = 0;
+    }
+    check_cursor(char_idx, cursor, col, pos_y + line_num, ' ');
+
+    tb.change_cell(
+        cursor_xychar.0,
+        cursor_xychar.1,
+        cursor_xychar.2,
+        colors.cursor.fg,
+        colors.cursor.bg,
+    );
+
+    tb.set_cursor(Some(((cursor_xychar.0) as u16, (cursor_xychar.1) as u16)));
+}
+
+pub(crate) fn draw_line_autocomplete(
+    original_buffer: &InputLine,
+    insertion_point: usize,
+    word_starts: usize,
+    ref completions: &Vec<String>,
+    current_completion: usize,
+    tb: &mut Termbox,
+    colors: &Colors,
+    pos_x: i32,
+    pos_y: i32,
+    width: i32,
+    cursor: i32,
+    should_scroll: bool,
+    scroll: Option<i32>,
+) {
+    let completion: &str = &completions[current_completion];
+
+    let mut orig_buf_iter = original_buffer.get_buffer().iter().cloned();
+    let mut completion_iter = completion.chars();
+
+    if should_scroll {
+        let cursor_x_off = cursor - scroll.unwrap();
+        let cursor_y_off = 0;
+
+        // draw a placeholder for the cursor
+        tb.change_cell(
+            pos_x + cursor_x_off,
+            pos_y + cursor_y_off,
+            ' ',
+            colors.user_msg.fg,
+            colors.user_msg.bg,
+        );
+
+        let iter: utils::InsertIterator<char> =
+            utils::insert_iter(&mut orig_buf_iter, &mut completion_iter, insertion_point);
+
+        for (char_idx, char) in iter.enumerate() {
+            let x_off;
+            let y_off;
+            let mut can_scroll = true;
+            if should_scroll {
+                let scroll = scroll.unwrap_or(0);
+                x_off = (char_idx as i32) - scroll;
+                y_off = 0;
+                if char_idx >= ((scroll + width) as usize) {
+                    break;
+                }
+                if char_idx < scroll as usize {
+                    can_scroll = false
+                }
+            } else {
+                x_off = char_idx as i32 % width;
+                y_off = char_idx as i32 / width;
+            }
+            if can_scroll {
+                if char_idx >= word_starts && char_idx < insertion_point + completion.len() {
+                    tb.change_cell(
+                        pos_x + x_off,
+                        pos_y + y_off,
+                        char,
+                        colors.completion.fg,
+                        colors.completion.bg,
+                    );
+                } else {
+                    tb.change_cell(
+                        pos_x + x_off,
+                        pos_y + y_off,
+                        char,
+                        colors.user_msg.fg,
+                        colors.user_msg.bg,
+                    );
+                }
+            }
+        }
+        tb.set_cursor(Some((
+            (pos_x + cursor_x_off) as u16,
+            (pos_y + cursor_y_off) as u16,
+        )));
+    } else {
+        // TODO: Think of a better way to handle this inefficiency
+        let iter: utils::InsertIterator<char> =
+            utils::insert_iter(&mut orig_buf_iter, &mut completion_iter, insertion_point);
+
+        let mut line = InputLine::from_buffer(iter.collect());
+        line.calculate_height(width, pos_x as usize);
+        let completion_range = CompletionRange {
+            start_idx: word_starts,
+            end_idx: (insertion_point + completion.len()),
+        };
+        draw_line_wrapped(
+            tb,
+            colors,
+            pos_x,
+            pos_y,
+            width,
+            cursor,
+            &line,
+            &Some(completion_range),
+        );
+    }
+}
+
+pub(crate) fn draw_line(
+    tb: &mut Termbox,
+    colors: &Colors,
+    line: &InputLine,
+    pos_x: i32,
+    pos_y: i32,
+    width: i32,
+    cursor: i32,
+    should_scroll: bool,
+    scroll: Option<i32>,
+    completion_range: Option<CompletionRange>,
+) {
+    if should_scroll || (line.len() as i32) < width - pos_x {
+        draw_line_scroll(
+            tb,
+            colors,
+            &line.buffer,
+            pos_x,
+            pos_y,
+            width,
+            cursor,
+            scroll.unwrap_or(0),
+        );
+    } else {
+        draw_line_wrapped(
+            tb,
+            colors,
+            pos_x,
+            pos_y,
+            width,
+            cursor,
+            &line,
+            &completion_range,
+        );
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::InputLine;
+    #[test]
+    fn test_calculate_height() {
+        let buffer: Vec<char> = String::from("012345").chars().collect();
+        let mut line = InputLine::from_buffer(buffer);
+
+        assert_eq!(line.calculate_height(6, 2), 2);
+
+        // one extra line for the cursor
+        assert_eq!(line.calculate_height(6, 0), 2);
+        assert_eq!(line.calculate_height(3, 0), 3);
+
+        line.insert(3, ' '); // 123 456
+        assert_eq!(line.calculate_height(6, 0), 2);
+
+        // Input containing lines that are longer than the width
+        let buffer: Vec<char> = String::from("01 3456").chars().collect();
+        let mut line = InputLine::from_buffer(buffer);
+        assert_eq!(line.calculate_height(3, 0), 3);
+        line.insert(line.len(), ' ');
+        // "01 3456 "
+        // Each line should be:
+        // "01 "
+        // "345"
+        // "6 "
+        assert_eq!(line.calculate_height(3, 0), 3);
+
+        line.insert(line.len(), '8');
+        line.insert(line.len(), '9');
+        line.insert(line.len(), 'X');
+        line.insert(line.len(), '1');
+        line.insert(line.len(), '2');
+        line.insert(line.len(), ' ');
+        line.insert(line.len(), '4');
+        // "01 3456 89X12 "
+        // Each line should be:
+        // "01 "
+        // "345"
+        // "6 "
+        // "89X"
+        // "12 "
+        // "4"
+        assert_eq!(line.calculate_height(3, 0), 6);
+
+        // First line has no whitespaces, but whitespaces follow
+        let buffer: Vec<char> = String::from("012345 78 X 12 34").chars().collect();
+        let mut line = InputLine::from_buffer(buffer);
+        // "0123"
+        // "45 "
+        // "78 X"
+        // " 12 "
+        // "34"
+        assert_eq!(line.calculate_height(4, 0), 5);
+    }
+}
diff --git a/libtiny_tui/src/text_field.rs b/libtiny_tui/src/input_area/mod.rs
similarity index 64%
rename from libtiny_tui/src/text_field.rs
rename to libtiny_tui/src/input_area/mod.rs
index a515164a..26601603 100644
--- a/libtiny_tui/src/text_field.rs
+++ b/libtiny_tui/src/input_area/mod.rs
@@ -6,32 +6,51 @@ use std::{
 use term_input::{Arrow, Key};
 use termbox_simple::Termbox;
 
-use crate::{config::Colors, termbox, trie::Trie, utils, widget::WidgetRet};
+use crate::{
+    config::{Colors, Style},
+    termbox,
+    trie::Trie,
+    utils,
+    widget::WidgetRet,
+};
+
+pub(crate) mod input_line;
+use self::input_line::{draw_line, draw_line_autocomplete, InputLine};
 
 /// Inspired by vim's 'scrolloff': minimal number of characters to keep above and below the cursor.
-const SCROLLOFF: i32 = 5;
+const SCROLL_OFF: i32 = 5;
+
+/// Minimum width of TextField for wrapping
+const SCROLL_FALLBACK_WIDTH: i32 = 36;
 
+/// Input history size
 const HIST_SIZE: usize = 30;
 
 pub(crate) struct TextField {
     /// The message that's currently being edited (not yet sent)
-    buffer: Vec<char>,
+    buffer: InputLine,
 
-    /// Cursor in currently shown line
     cursor: i32,
 
-    /// Horizontal scroll
-    scroll: i32,
-
     /// Width of the widget
     width: i32,
+    /// Height of the widget, invalidated on input and resize
+    height: Option<i32>,
+
+    /// Maximum lines the widget can grow to
+    max_lines: i32,
+
+    /// Amount of scroll in the input field
+    scroll: Option<i32>,
 
     /// A history of sent messages/commands. Once added messages are never
     /// modified. A modification attempt should result in a new buffer with a
     /// copy of the vector in history. (old contents of the buffer will be lost)
-    history: Vec<Vec<char>>,
+    history: Vec<InputLine>,
 
     mode: Mode,
+
+    nick: Option<Nickname>,
 }
 
 enum Mode {
@@ -43,7 +62,7 @@ enum Mode {
 
     /// Auto-completing a nick in channel
     Autocomplete {
-        original_buffer: Vec<char>,
+        original_buffer: InputLine,
         insertion_point: usize,
         word_starts: usize,
         completions: Vec<String>,
@@ -51,36 +70,101 @@ enum Mode {
     },
 }
 
+pub(crate) struct Nickname {
+    value: String,
+    color: usize,
+}
+
+static NICKNAME_SUFFIX: &str = ": ";
+impl Nickname {
+    fn new(value: String, color: usize) -> Nickname {
+        Nickname { value, color }
+    }
+
+    /// Calculates the length of the nickname based on given width, including the NICKNAME_SUFFIX
+    /// width should be the width of TextField
+    /// When the length of the Nickname area is 30% or less of the width, then show
+    fn len(&self, width: i32) -> usize {
+        let len = self.value.len() + NICKNAME_SUFFIX.len();
+        if len as f32 <= width as f32 * (30f32 / 100f32) {
+            len
+        } else {
+            0
+        }
+    }
+
+    fn draw(&self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32, width: i32) {
+        if self.len(width) > 0 {
+            let nick_color = colors.nick[self.color % colors.nick.len()];
+            let style = Style {
+                fg: u16::from(nick_color),
+                bg: colors.user_msg.bg,
+            };
+            termbox::print_chars(
+                tb,
+                pos_x,
+                pos_y,
+                style,
+                self.value.chars().chain(NICKNAME_SUFFIX.chars()),
+            );
+        }
+    }
+}
+
 impl TextField {
-    pub(crate) fn new(width: i32) -> TextField {
+    pub(crate) fn new(width: i32, max_lines: i32) -> TextField {
         TextField {
-            buffer: Vec::with_capacity(512),
+            buffer: InputLine::new(),
             cursor: 0,
-            scroll: 0,
             width,
+            height: Some(1),
+            max_lines,
+            scroll: None,
             history: Vec::with_capacity(HIST_SIZE),
             mode: Mode::Edit,
+            nick: None,
+        }
+    }
+
+    pub(crate) fn set_nick(&mut self, value: String, color: usize) {
+        self.nick = Some(Nickname::new(value, color))
+    }
+
+    pub(crate) fn get_nick(&self) -> Option<String> {
+        if let Some(nick) = &self.nick {
+            let nick_string = nick.value.clone();
+            Some(nick_string)
+        } else {
+            None
         }
     }
 
-    pub(crate) fn resize(&mut self, width: i32) {
+    /// Resizes TextArea
+    pub(crate) fn resize(&mut self, width: i32, max_lines: i32) {
         self.width = width;
-        let cursor = self.cursor;
-        self.move_cursor(cursor);
+        self.height = None;
+        self.max_lines = max_lines;
     }
 
     pub(crate) fn draw(&self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
+        let mut nick_length = 0;
+        if let Some(nick) = &self.nick {
+            nick.draw(tb, colors, pos_x, pos_y, self.width);
+            nick_length = nick.len(self.width) as i32;
+        }
         match self.mode {
             Mode::Edit => {
                 draw_line(
                     tb,
                     colors,
                     &self.buffer,
-                    pos_x,
+                    pos_x + nick_length,
                     pos_y,
-                    self.scroll,
                     self.width,
                     self.cursor,
+                    self.should_scroll(),
+                    self.scroll,
+                    None,
                 );
             }
             Mode::History(hist_curs) => {
@@ -88,11 +172,13 @@ impl TextField {
                     tb,
                     colors,
                     &self.history[hist_curs as usize],
-                    pos_x,
+                    pos_x + nick_length,
                     pos_y,
-                    self.scroll,
                     self.width,
                     self.cursor,
+                    self.should_scroll(),
+                    self.scroll,
+                    None,
                 );
             }
             Mode::Autocomplete {
@@ -101,78 +187,41 @@ impl TextField {
                 word_starts,
                 ref completions,
                 current_completion,
-            } => {
-                // draw a placeholder for the cursor
-                tb.change_cell(
-                    pos_x + self.cursor - self.scroll,
-                    pos_y,
-                    ' ',
-                    colors.user_msg.fg,
-                    colors.user_msg.bg,
-                );
-
-                let completion: &str = &completions[current_completion];
-
-                let mut orig_buf_iter = original_buffer.iter().cloned();
-                let mut completion_iter = completion.chars();
-
-                let iter: utils::InsertIterator<char> =
-                    utils::insert_iter(&mut orig_buf_iter, &mut completion_iter, insertion_point);
-
-                for (char_idx, char) in iter.enumerate() {
-                    if char_idx >= ((self.scroll + self.width) as usize) {
-                        break;
-                    }
-
-                    if char_idx >= self.scroll as usize {
-                        if char_idx >= word_starts && char_idx < insertion_point + completion.len()
-                        {
-                            tb.change_cell(
-                                pos_x + (char_idx as i32) - self.scroll,
-                                pos_y,
-                                char,
-                                colors.completion.fg,
-                                colors.completion.bg,
-                            );
-                        } else {
-                            tb.change_cell(
-                                pos_x + (char_idx as i32) - self.scroll,
-                                pos_y,
-                                char,
-                                colors.user_msg.fg,
-                                colors.user_msg.bg,
-                            );
-                        }
-                    }
-                }
-
-                tb.set_cursor(Some((
-                    (pos_x + self.cursor - self.scroll) as u16,
-                    pos_y as u16,
-                )));
-            }
+            } => draw_line_autocomplete(
+                original_buffer,
+                insertion_point,
+                word_starts,
+                completions,
+                current_completion,
+                tb,
+                colors,
+                pos_x + nick_length,
+                pos_y,
+                self.width,
+                self.cursor,
+                self.should_scroll(),
+                self.scroll,
+            ),
         }
     }
 
     pub(crate) fn keypressed(&mut self, key: Key) -> WidgetRet {
         match key {
             Key::Char('\r') => {
-                if self.line_len() > 0 {
+                if self.current_buffer_len() > 0 {
                     self.modify();
 
-                    let ret = mem::replace(&mut self.buffer, Vec::new());
+                    let ret = mem::replace(&mut self.buffer, InputLine::new());
                     if self.history.len() == HIST_SIZE {
-                        let mut reuse = self.history.remove(0);
-                        reuse.clear();
-                        reuse.extend_from_slice(&ret);
-                        self.history.push(reuse);
+                        self.history.remove(0);
+                        self.history.push(ret.clone());
                     } else {
                         self.history.push(ret.clone());
                     }
 
                     self.move_cursor(0);
 
-                    WidgetRet::Input(ret)
+                    WidgetRet::Input(ret.get_buffer().to_vec())
                 } else {
                     WidgetRet::KeyHandled
                 }
@@ -195,7 +244,7 @@ impl TextField {
             }
 
             Key::Del => {
-                if self.cursor < self.line_len() {
+                if self.cursor < self.current_buffer_len() {
                     self.modify();
                     self.buffer.remove(self.cursor as usize);
                     // TODO: We should probably call move_cursor here to update scroll?
@@ -211,7 +260,7 @@ impl TextField {
                     self.move_cursor_to_end();
                     WidgetRet::KeyHandled
                 } else if ch == 'k' {
-                    if self.cursor != self.line_len() {
+                    if self.cursor != self.current_buffer_len() {
                         self.modify();
                         self.buffer.drain(self.cursor as usize..);
                     }
@@ -255,7 +304,7 @@ impl TextField {
             }
 
             Key::CtrlArrow(Arrow::Right) => {
-                let len = self.line_len() as usize;
+                let len = self.current_buffer_len() as usize;
                 if (self.cursor as usize) < len {
                     let mut cur = self.cursor as usize;
                     let mut skipped = false;
@@ -278,6 +327,8 @@ impl TextField {
             ////////////////////////////////////////////////////////////////////
             // Scrolling in history or autocompletion list
             Key::Arrow(Arrow::Up) => {
+                // invalidate height calculation
+                self.height = None;
                 let mode = mem::replace(&mut self.mode, Mode::Edit);
 
                 match mode {
@@ -328,6 +379,8 @@ impl TextField {
             }
 
             Key::Arrow(Arrow::Down) => {
+                // invalidate height calculation
+                self.height = None;
                 let mode = mem::replace(&mut self.mode, Mode::Edit);
 
                 match mode {
@@ -385,12 +438,13 @@ impl TextField {
 
     /// Add a line to the text field history.
     pub(crate) fn add_history(&mut self, str: &str) {
-        self.history.push(str.chars().collect());
+        self.history
+            .push(InputLine::from_buffer(str.chars().collect()));
     }
 
     pub(crate) fn set(&mut self, str: &str) {
         self.mode = Mode::Edit;
-        self.buffer = str.chars().collect();
+        self.buffer = InputLine::from_buffer(str.chars().collect());
         self.move_cursor_to_end();
     }
 
@@ -402,14 +456,14 @@ impl TextField {
 
         self.modify();
 
-        let char = self.buffer[(self.cursor - 1) as usize];
+        let char = self.buffer.get((self.cursor - 1) as usize);
 
         // Try to imitate vim's behaviour here.
         if char.is_whitespace() {
             self.consume_before(char::is_whitespace);
             self.consume_before(char::is_alphanumeric);
         } else {
-            let char = self.buffer[(self.cursor - 1) as usize];
+            let char = self.buffer.get((self.cursor - 1) as usize);
             if char.is_alphanumeric() {
                 self.consume_before(char::is_alphanumeric);
             } else if self.cursor != 0 {
@@ -427,7 +481,7 @@ impl TextField {
     {
         let end_range = self.cursor as usize;
         let mut begin_range = self.cursor - 1;
-        while begin_range >= 0 && f(self.buffer[begin_range as usize]) {
+        while begin_range >= 0 && f(self.buffer.get(begin_range as usize)) {
             begin_range -= 1;
         }
         self.buffer.drain(((begin_range + 1) as usize)..end_range);
@@ -435,14 +489,81 @@ impl TextField {
     }
 
     // Ignoring auto-completions
-    fn shown_line(&self) -> &Vec<char> {
-        match self.mode {
-            Mode::Edit | Mode::Autocomplete { .. } => &self.buffer,
-            Mode::History(hist_curs) => &self.history[hist_curs as usize],
+    pub(crate) fn shown_line(&mut self) -> &mut InputLine {
+        match &mut self.mode {
+            Mode::Edit | Mode::Autocomplete { .. } => &mut self.buffer,
+            Mode::History(hist_curs) => &mut self.history[*hist_curs as usize],
+        }
+    }
+
+    fn calculate_height_autocomplete(&self, width: i32, nick_length: usize) -> usize {
+        if let Mode::Autocomplete {
+            original_buffer,
+            completions,
+            current_completion,
+            word_starts,
+            ..
+        } = &self.mode
+        {
+            let mut temp_buffer = original_buffer.clone();
+            let completion = &completions[*current_completion];
+            for (idx, c) in completion.char_indices() {
+                temp_buffer.insert(word_starts + idx, c);
+            }
+            temp_buffer.calculate_height(width, nick_length)
+        } else {
+            1
+        }
+    }
+
+    /// Gets the height of the widget
+    /// If the height is larger than the allowed max lines turn on scroll
+    /// else turn off scroll
+    pub(crate) fn get_height(&mut self, width: i32) -> i32 {
+        let height = match self.height {
+            Some(height) => height,
+            None => self.calculate_height(width),
+        };
+
+        // Check for scroll fallback
+        if height >= self.max_lines || width <= SCROLL_FALLBACK_WIDTH {
+            self.scroll_on();
+            1
+        } else {
+            self.scroll_off();
+            height
+        }
+    }
+
+    fn scroll_on(&mut self) {
+        // If scroll is already on, we don't need to calculate scroll
+        if let None = self.scroll {
+            self.scroll = Some(0);
+            self.move_cursor(self.cursor);
+        }
+    }
+
+    fn scroll_off(&mut self) {
+        self.scroll = None;
+    }
+
+    fn calculate_height(&mut self, width: i32) -> i32 {
+        let mut line_count: i32 = 1;
+        let mut nick_length = 0;
+        if let Some(nick) = &self.nick {
+            nick_length = nick.len(self.width);
+        }
+        if self.current_buffer_len() >= width - nick_length as i32 {
+            if self.in_autocomplete() {
+                line_count = self.calculate_height_autocomplete(width, nick_length) as i32;
+            } else {
+                line_count = self.shown_line().calculate_height(width, nick_length) as i32;
+            }
         }
+        line_count
     }
 
-    fn line_len(&self) -> i32 {
+    fn current_buffer_len(&self) -> i32 {
         match self.mode {
             Mode::Edit => self.buffer.len() as i32,
             Mode::History(hist_curs) => self.history[hist_curs as usize].len() as i32,
@@ -457,8 +578,8 @@ impl TextField {
 
     fn char_at(&self, idx: usize) -> char {
         match self.mode {
-            Mode::Edit => self.buffer[idx],
-            Mode::History(hist_curs) => self.history[hist_curs as usize][idx],
+            Mode::Edit => self.buffer.get(idx),
+            Mode::History(hist_curs) => self.history[hist_curs as usize].get(idx),
             Mode::Autocomplete {
                 ref original_buffer,
                 insertion_point,
@@ -467,7 +588,7 @@ impl TextField {
                 ..
             } => {
                 if idx < insertion_point {
-                    original_buffer[idx]
+                    original_buffer.get(idx)
                 } else if idx >= insertion_point
                     && idx < insertion_point + completions[current_completion].len()
                 {
@@ -476,7 +597,7 @@ impl TextField {
                         .nth(idx - insertion_point)
                         .unwrap()
                 } else {
-                    original_buffer[idx - completions[current_completion].len()]
+                    original_buffer.get(idx - completions[current_completion].len())
                 }
             }
         }
@@ -492,12 +613,12 @@ impl TextField {
     }
 
     fn modify(&mut self) {
+        // invalidate height calculation
+        self.height = None;
         match self.mode {
             Mode::Edit => {}
             Mode::History(hist_idx) => {
-                self.buffer.clear();
-                self.buffer
-                    .extend_from_slice(&self.history[hist_idx as usize]);
+                self.buffer = self.history[hist_idx as usize].clone();
             }
             Mode::Autocomplete {
                 ref mut original_buffer,
@@ -506,7 +627,7 @@ impl TextField {
                 current_completion,
                 ..
             } => {
-                let mut buffer: Vec<char> = mem::replace(original_buffer, vec![]);
+                let mut buffer = mem::replace(original_buffer, InputLine::new());
                 let completions: Vec<String> = mem::replace(completions, vec![]);
                 let word = &completions[current_completion];
 
@@ -516,10 +637,9 @@ impl TextField {
                     insertion_point += 1;
                 }
 
-                self.buffer = buffer;
+                self.buffer = buffer
             }
         }
-
         self.mode = Mode::Edit;
     }
 
@@ -527,7 +647,7 @@ impl TextField {
     // Manipulating cursor
 
     fn inc_cursor(&mut self) {
-        let cur = min(self.line_len(), self.cursor + 1);
+        let cur = min(self.current_buffer_len(), self.cursor + 1);
         self.move_cursor(cur);
     }
 
@@ -537,80 +657,59 @@ impl TextField {
     }
 
     fn move_cursor_to_end(&mut self) {
-        let cursor = self.line_len();
+        let cursor = self.current_buffer_len();
         self.move_cursor(cursor);
     }
 
     /// Update cursor location, possibly after an update. Update scroll value to fit as much of the
     /// input field as possible to the screen.
     fn move_cursor(&mut self, cursor: i32) {
-        let line_len = self.line_len();
+        let line_len = self.current_buffer_len();
 
-        // Cursor should be in range
         assert!(cursor >= 0 && cursor <= line_len);
         self.cursor = cursor;
 
-        // Disable SCROLLOFF if there isn't enough space on the screen to have SCROLLOFF space on
-        // both ends
-        let scrolloff = {
-            if self.width < 2 * SCROLLOFF + 1 {
-                0
+        if self.scroll.is_some() {
+            let mut nick_length = 0;
+            if let Some(nick) = &self.nick {
+                nick_length = nick.len(self.width);
+            }
+            let fixed_width = self.width - nick_length as i32;
+            if self.current_buffer_len() + 1 >= fixed_width {
+                // Disable SCROLLOFF if there isn't enough space on the screen to have SCROLLOFF space on
+                // both ends
+                let scrolloff = {
+                    if self.width < 2 * SCROLL_OFF + 1 {
+                        0
+                    } else {
+                        SCROLL_OFF
+                    }
+                };
+
+                // Shown range of the text field before updating scroll
+                let left_end = min(self.scroll.unwrap(), line_len);
+                let right_end = min(self.scroll.unwrap() + fixed_width, line_len);
+
+                if cursor - scrolloff < left_end {
+                    self.scroll = Some(max(0, cursor - scrolloff));
+                } else if cursor + scrolloff >= right_end {
+                    let scroll = min(
+                        // +1 because cursor should be visible, i.e.
+                        // right_end > cursor should hold after this
+                        max(0, cursor + 1 + scrolloff - fixed_width),
+                        // +1 because cursor goes one more character
+                        // after the buffer, to be able to add chars
+                        max(0, self.current_buffer_len() + 1 - fixed_width),
+                    );
+                    self.scroll = Some(scroll);
+                }
             } else {
-                SCROLLOFF
+                self.scroll = Some(0);
             }
         };
-
-        // Shown range of the text field before updating scroll
-        let left_end = min(self.scroll, line_len);
-        let right_end = min(self.scroll + self.width, line_len);
-
-        // println!(
-        //     "scrolloff={}, left_end={}, right_end={}",
-        //     scrolloff, left_end, right_end
-        // );
-
-        if cursor - scrolloff < left_end {
-            self.scroll = max(0, cursor - scrolloff);
-        } else if cursor + scrolloff >= right_end {
-            self.scroll = min(
-                // +1 because cursor should be visible, i.e.
-                // right_end > cursor should hold after this
-                max(0, cursor + 1 + scrolloff - self.width),
-                // +1 because cursor goes one more character
-                // after the buffer, to be able to add chars
-                max(0, self.line_len() + 1 - self.width),
-            );
-        }
     }
 }
 
-fn draw_line(
-    tb: &mut Termbox,
-    colors: &Colors,
-    line: &[char],
-    pos_x: i32,
-    pos_y: i32,
-    scroll: i32,
-    width: i32,
-    cursor: i32,
-) {
-    let slice: &[char] = &line[scroll as usize..min(line.len(), (scroll + width) as usize)];
-    termbox::print_chars(tb, pos_x, pos_y, colors.user_msg, slice.iter().cloned());
-
-    // On my terminal the cursor is only shown when there's a character
-    // under it.
-    if cursor as usize >= line.len() {
-        tb.change_cell(
-            pos_x + cursor - scroll,
-            pos_y,
-            ' ',
-            colors.cursor.fg,
-            colors.cursor.bg,
-        );
-    }
-    tb.set_cursor(Some(((pos_x + cursor - scroll) as u16, pos_y as u16)));
-}
-
 impl TextField {
     pub(crate) fn autocomplete(&mut self, dict: &Trie) {
         if self.in_autocomplete() {
@@ -619,11 +718,14 @@ impl TextField {
             return;
         }
 
+        // invalidate height calculation
+        self.height = None;
+
         let cursor_right = self.cursor;
         let mut cursor_left = max(0, cursor_right - 1);
 
         let completions = {
-            let line = self.shown_line();
+            let line = &self.shown_line().get_buffer();
 
             while cursor_left >= 0
                 && line
@@ -659,6 +761,10 @@ impl TextField {
             self.move_cursor(cursor + completion_len as i32);
         }
     }
+
+    fn should_scroll(&self) -> bool {
+        self.scroll.is_some()
+    }
 }
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -671,7 +777,7 @@ mod tests {
 
     #[test]
     fn text_field_bug() {
-        let mut text_field = TextField::new(10);
+        let mut text_field = TextField::new(10, 50);
         text_field.keypressed(Key::Char('a'));
         text_field.keypressed(Key::Char(' '));
         text_field.keypressed(Key::Char('b'));
diff --git a/libtiny_tui/src/lib.rs b/libtiny_tui/src/lib.rs
index 1af36a78..81cc1cf0 100644
--- a/libtiny_tui/src/lib.rs
+++ b/libtiny_tui/src/lib.rs
@@ -5,15 +5,15 @@
 
 mod config;
 mod exit_dialogue;
+mod input_area;
 mod messaging;
-// FIXME: This is "pub" to be able to use in an example
 #[doc(hidden)]
+// FIXME: This is "pub" to be able to use in an example
 pub mod msg_area;
 mod notifier;
 mod statusline;
 mod tab;
 mod termbox;
-mod text_field;
 mod trie;
 mod tui;
 mod utils;
diff --git a/libtiny_tui/src/messaging.rs b/libtiny_tui/src/messaging.rs
index b07a2f65..5abea246 100644
--- a/libtiny_tui/src/messaging.rs
+++ b/libtiny_tui/src/messaging.rs
@@ -6,14 +6,13 @@ use std::convert::From;
 use time::{self, Tm};
 
 use crate::{
-    config::{Colors, Style},
+    config::Colors,
     exit_dialogue::ExitDialogue,
+    input_area::TextField,
     msg_area::{
         line::{SchemeStyle, SegStyle},
         MsgArea,
     },
-    termbox,
-    text_field::TextField,
     trie::Trie,
     widget::WidgetRet,
 };
@@ -39,9 +38,6 @@ pub(crate) struct MessagingUI {
     // properly highlight mentions.
     nicks: Trie,
 
-    current_nick: Option<String>,
-    show_current_nick: bool,
-
     last_activity_line: Option<ActivityLine>,
     last_activity_ts: Option<Timestamp>,
 }
@@ -81,67 +77,47 @@ impl MessagingUI {
     pub(crate) fn new(width: i32, height: i32, status: bool) -> MessagingUI {
         MessagingUI {
             msg_area: MsgArea::new(width, height - 1),
-            input_field: TextField::new(width),
+            input_field: TextField::new(width, get_input_field_max_height(height)),
             exit_dialogue: None,
             width,
             height,
             show_status: status,
             nicks: Trie::new(),
-            current_nick: None,
-            show_current_nick: true,
             last_activity_line: None,
             last_activity_ts: None,
         }
     }
 
     pub(crate) fn set_nick(&mut self, nick: String) {
-        self.current_nick = Some(nick);
+        let nick_color = self.get_nick_color(&nick);
+        self.input_field.set_nick(nick, nick_color);
         // update text field size
         let w = self.width;
         let h = self.height;
         self.resize(w, h);
     }
 
-    pub(crate) fn get_nick(&self) -> Option<&str> {
-        self.current_nick.as_deref()
+    pub(crate) fn get_nick(&self) -> Option<String> {
+        self.input_field.get_nick()
     }
 
-    pub(crate) fn draw(&self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
+    pub(crate) fn draw(&mut self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
+        let input_field_height = self.input_field.get_height(self.width);
+
+        // if the height of msg_area needs to change...
+        if self.height - input_field_height != self.msg_area.get_height() {
+            self.msg_area
+                .resize(self.width, self.height - input_field_height);
+        }
         self.msg_area.draw(tb, colors, pos_x, pos_y);
 
-        let input_field_y = pos_y + self.height - 1;
+        let input_field_y = pos_y + self.height - input_field_height;
         match &self.exit_dialogue {
             Some(exit_dialogue) => {
                 exit_dialogue.draw(tb, colors, pos_x, input_field_y);
             }
             None => {
-                if let Some(ref nick) = self.current_nick {
-                    if self.show_current_nick {
-                        let nick_color = colors.nick[self.get_nick_color(nick) % colors.nick.len()];
-                        let style = Style {
-                            fg: u16::from(nick_color),
-                            bg: colors.user_msg.bg,
-                        };
-                        termbox::print_chars(tb, pos_x, input_field_y, style, nick.chars());
-                        tb.change_cell(
-                            pos_x + nick.len() as i32,
-                            input_field_y,
-                            ':',
-                            colors.user_msg.fg,
-                            colors.user_msg.bg,
-                        );
-                        self.input_field.draw(
-                            tb,
-                            colors,
-                            pos_x + nick.len() as i32 + 2,
-                            input_field_y,
-                        );
-                    } else {
-                        self.input_field.draw(tb, colors, pos_x, input_field_y);
-                    }
-                } else {
-                    self.input_field.draw(tb, colors, pos_x, input_field_y);
-                }
+                self.input_field.draw(tb, colors, pos_x, input_field_y);
             }
         }
     }
@@ -212,26 +188,12 @@ impl MessagingUI {
     pub(crate) fn resize(&mut self, width: i32, height: i32) {
         self.width = width;
         self.height = height;
-        self.msg_area.resize(width, height - 1);
-
-        let nick_width = match self.current_nick {
-            None => 0,
-            Some(ref rc) =>
-            // +2 for ": "
-            {
-                rc.len() as i32 + 2
-            }
-        };
 
-        self.show_current_nick = (nick_width as f32) <= (width as f32) * (30f32 / 100f32);
-
-        let widget_width = if self.show_current_nick {
-            width - nick_width
-        } else {
-            width
-        };
-
-        self.input_field.resize(widget_width);
+        self.input_field
+            .resize(width, get_input_field_max_height(height));
+        // msg_area should resize based on input_field's rendered height
+        let msg_area_height = height - self.input_field.get_height(width);
+        self.msg_area.resize(width, msg_area_height);
 
         // We don't show the nick in exit dialogue, so it has the full width
         for exit_dialogue in &mut self.exit_dialogue {
@@ -263,6 +225,11 @@ impl MessagingUI {
     }
 }
 
+/// Calculation for input field's maximum height
+fn get_input_field_max_height(window_height: i32) -> i32 {
+    window_height / 2
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 // Adding new messages
 
diff --git a/libtiny_tui/src/msg_area/mod.rs b/libtiny_tui/src/msg_area/mod.rs
index c72bfd7c..f72787d2 100644
--- a/libtiny_tui/src/msg_area/mod.rs
+++ b/libtiny_tui/src/msg_area/mod.rs
@@ -38,6 +38,10 @@ impl MsgArea {
         }
     }
 
+    pub(crate) fn get_height(&self) -> i32 {
+        self.height
+    }
+
     pub(crate) fn resize(&mut self, width: i32, height: i32) {
         self.width = width;
         self.height = height;
diff --git a/libtiny_tui/src/tui.rs b/libtiny_tui/src/tui.rs
index 85acc138..6357ae59 100644
--- a/libtiny_tui/src/tui.rs
+++ b/libtiny_tui/src/tui.rs
@@ -227,9 +227,7 @@ impl TUI {
                         &MsgTarget::CurrentTab,
                     );
                 }
-                Ok(Config { colors }) => {
-                    self.set_colors(colors);
-                }
+                Ok(Config { colors }) => self.set_colors(colors),
             }
         }
     }
@@ -367,8 +365,7 @@ impl TUI {
                     if self.active_idx >= tab_idx {
                         self.next_tab();
                     }
-                    if let Some(nick) = self.tabs[serv_tab_idx].widget.get_nick().map(str::to_owned)
-                    {
+                    if let Some(nick) = self.tabs[serv_tab_idx].widget.get_nick() {
                         self.tabs[tab_idx].widget.set_nick(nick);
                     }
                     Some(tab_idx)
@@ -407,7 +404,7 @@ impl TUI {
                         Notifier::Messages,
                         None,
                     );
-                    if let Some(nick) = self.tabs[tab_idx].widget.get_nick().map(str::to_owned) {
+                    if let Some(nick) = self.tabs[tab_idx].widget.get_nick() {
                         self.tabs[tab_idx + 1].widget.set_nick(nick);
                     }
                     self.tabs[tab_idx + 1].widget.join(nick, None);
@@ -1146,7 +1143,7 @@ impl TUI {
             let nick = tab.widget.get_nick();
             if let Some(nick_) = nick {
                 tab.notifier
-                    .notify_privmsg(sender, msg, target, nick_, highlight);
+                    .notify_privmsg(sender, msg, target, &nick_, highlight);
             }
         });
     }


Test patch:
diff --git a/libtiny_tui/src/tests/mod.rs b/libtiny_tui/src/tests/mod.rs
index 540a30b0..a7e2a2f9 100644
--- a/libtiny_tui/src/tests/mod.rs
+++ b/libtiny_tui/src/tests/mod.rs
@@ -3,7 +3,7 @@ use crate::tui::TUI;
 use libtiny_ui::*;
 use term_input::{Event, Key};
 use termbox_simple::CellBuf;
-use time;
+use time::Tm;
 
 use std::panic::Location;
 
@@ -155,6 +155,7 @@ fn small_screen_1() {
         "|00:00 +123456 +abcdef|
          |osa1:                |
          |< #chan              |";
+
     expect_screen(screen, &tui, 21, 3, Location::caller());
 
     tui.set_size(24, 3);
@@ -165,6 +166,7 @@ fn small_screen_1() {
         "|00:00 +123456 +abcdef   |
          |osa1:                   |
          |< irc.server_1.org #chan|";
+
     expect_screen(screen, &tui, 24, 3, Location::caller());
 
     tui.set_size(31, 3);
@@ -175,6 +177,7 @@ fn small_screen_1() {
         "|00:00 +123456 +abcdef          |
          |osa1:                          |
          |mentions irc.server_1.org #chan|";
+
     expect_screen(screen, &tui, 31, 3, Location::caller());
 }
 
@@ -281,3 +284,157 @@ fn ctrl_w() {
 
     expect_screen(screen, &tui, 30, 3, Location::caller());
 }
+
+// Tests text field wrapping (text_field_wrap setting)
+#[test]
+fn test_text_field_wrap() {
+    // Screen should be wide enough to enable wrapping. See SCROLL_FALLBACK_WIDTH in text_field.rs
+    let mut tui = TUI::new_test(40, 8);
+
+    let server = "chat.freenode.net";
+    tui.new_server_tab(server, None);
+    tui.set_nick(server, "x");
+
+    // Switch to server tab
+    tui.next_tab();
+
+    // Write some stuff
+    let target = MsgTarget::CurrentTab;
+    let ts: Tm = unsafe { ::std::mem::zeroed() };
+    tui.add_msg("test test test", ts, &target);
+
+    for _ in 0..37 {
+        let event = term_input::Event::Key(Key::Char('a'));
+        tui.handle_input_event(event);
+    }
+    for _ in 0..5 {
+        let event = term_input::Event::Key(Key::Char('b'));
+        tui.handle_input_event(event);
+    }
+
+    tui.draw();
+
+    #[rustfmt::skip]
+    let screen =
+    "|                                        |
+     |                                        |
+     |                                        |
+     |                                        |
+     |00:00 test test test                    |
+     |x: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa|
+     |bbbbb                                   |
+     |mentions chat.freenode.net              |";
+
+    expect_screen(screen, &tui, 40, 8, Location::caller());
+
+    // Test resizing
+    tui.set_size(46, 8);
+    tui.draw();
+
+    #[rustfmt::skip]
+    let screen =
+    "|                                              |
+     |                                              |
+     |                                              |
+     |                                              |
+     |                                              |
+     |00:00 test test test                          |
+     |x: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbb |
+     |mentions chat.freenode.net                    |";
+
+    expect_screen(screen, &tui, 46, 8, Location::caller());
+
+    // Reset size
+    tui.set_size(40, 8);
+
+    // If we remove a few characters now the line above the text field should still be right above
+    // the text field
+    for _ in 0..6 {
+        let event = term_input::Event::Key(Key::Backspace);
+        tui.handle_input_event(event);
+    }
+
+    tui.draw();
+
+    #[rustfmt::skip]
+    let screen =
+    "|                                        |
+     |                                        |
+     |                                        |
+     |                                        |
+     |                                        |
+     |00:00 test test test                    |
+     |x: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
+     |mentions chat.freenode.net              |";
+
+    expect_screen(screen, &tui, 40, 8, Location::caller());
+
+    // On making screen smaller we should fall back to scrolling
+    tui.set_size(30, 8);
+    for _ in 0..5 {
+        let event = term_input::Event::Key(Key::Char('b'));
+        tui.handle_input_event(event);
+    }
+    tui.draw();
+
+    #[rustfmt::skip]
+    let screen =
+    "|                              |
+     |                              |
+     |                              |
+     |                              |
+     |                              |
+     |00:00 test test test          |
+     |x: aaaaaaaaaaaaaaaaaaaaabbbbb |
+     |mentions chat.freenode.net    |";
+
+    expect_screen(screen, &tui, 30, 8, Location::caller());
+
+    tui.set_size(40, 8);
+    tui.draw();
+
+    #[rustfmt::skip]
+    let screen =
+    "|                                        |
+     |                                        |
+     |                                        |
+     |                                        |
+     |00:00 test test test                    |
+     |x: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab|
+     |bbbb                                    |
+     |mentions chat.freenode.net              |";
+
+    expect_screen(screen, &tui, 40, 8, Location::caller());
+
+    // Wrapping on words - splits lines on whitespace
+    for _ in 0..6 {
+        let event = term_input::Event::Key(Key::Backspace);
+        tui.handle_input_event(event);
+    }
+
+    let event = term_input::Event::Key(Key::Char(' '));
+    tui.handle_input_event(event);
+
+    for _ in 0..5 {
+        let event = term_input::Event::Key(Key::Char('b'));
+        tui.handle_input_event(event);
+    }
+
+    tui.draw();
+
+    #[rustfmt::skip]
+    let screen =
+    "|                                        |
+     |                                        |
+     |                                        |
+     |                                        |
+     |00:00 test test test                    |
+     |x: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa  |
+     |bbbbb                                   |
+     |mentions chat.freenode.net              |";
+
+    expect_screen(screen, &tui, 40, 8, Location::caller());
+
+    // TODO: Test changing nick (osa: I don't understand how nick length is taken into account when
+    // falling back to scrolling)
+}