Repository: osa1/tiny (language: rust)

Issue:
aligned table display
Is this possible to be able to 
- enable clock at each line in conf file
- enable align and specified nb of char for nick place as `` align: 15 ``

**from**
```                                                                                                
19:29 ahub: c'est quoi ton client eoli3n__ ?                                                                          
19:30 eoli3n__: https://github.com/osa1/tiny || tiny-irc-client-git AUR                                               
eoli3n__: je viens de l'installer                                                                                     
ahub: connais pas                                                                                                     
ahub: c'est quoi la différence avec un weechat ou autre ?                                                             
eoli3n__: je viens de decouvrir c'est top                                                                             
eoli3n__: ahub un simple fichier de conf, pas besoin de plugin pour gérer les highlight et les notif                  
ahub: ho                                                                                                             
19:31 ahub: ok :)                                                                                                     
19:32 -ahub                                                                                         
eoli3n__:    
```
**to**
```                                                                                                
19:29 ahub:        c'est quoi ton client eoli3n__ ?                                                                          
19:30 eoli3n__:    https://github.com/osa1/tiny || tiny-irc-client-git AUR                                               
19:30 eoli3n__:    je viens de l'installer                                                                                     
19:30 ahub:        connais pas                                                                                                     
19:30 ahub:        c'est quoi la différence avec un weechat ou autre ?                                                             
19:30 eoli3n__:    je viens de decouvrir c'est top                                                                             
19:30 eoli3n__:    ahub un simple fichier de conf, pas besoin de plugin pour gérer les highlight et les notif                  
19:31 ahub:        ho                                                                                                             
19:31 ahub:        ok :)                                                                                                     
19:32 -ahub                                                                                         
eoli3n__:    
```


Discussion / hints:
Another formatting idea. Perhaps it isn't as compact and "tiny" as it should be though...
```
19:29 |      ahub: c'est quoi ton client eoli3n__ ?                                                                          
19:30 |  eoli3n__: https://github.com/osa1/tiny || tiny-irc-client-git AUR                                               
19:30 |  eoli3n__: je viens de l'installer                                                                                     
19:30 |      ahub: connais pas                                                                                                     
19:30 |      ahub: c'est quoi la différence avec un weechat ou autre ?                                                             
19:30 |  eoli3n__: je viens de decouvrir c'est top                                                                             
19:30 |  eoli3n__: ahub un simple fichier de conf, pas besoin de plugin pour gérer les highlight et les
      |            notif                  
19:31 |      ahub: ho                                                                                                             
19:31 |      ahub: ok :)                                                                                                     
19:32 |  -ahub                                                                                         
eoli3n__:  bonjour
```
> Perhaps it isn't as compact and "tiny" as it should be though...

Indeed, this has been my main concern for this feature request. I'm not opposed to it though, as long as it's behind a flag/setting and not the default.

If we get a lot of feature requests like this an alternative would be to build a new TUI for tiny with fancier rendering (not as compact or simple). In `gui` branch I'm experimenting with implementing another UI (a GUI) to tiny, and it seems possible with not a lot of changes in the existing code. So I suspect adding another TUI implementation would similarly not require a lot of changes in the existing code.
how could  a ``gui`` be tinyier than cli ? :) will tiny stay a cli irc client or you will move it to a gui one ?

I'm highly waiting for this and for https://github.com/osa1/tiny/issues/167 to use tiny as my main IRC client, i can't weechat anymore, is that so hard to implement ? Please consider add an option to format output, and remove C-* keybinds, to let tiny use term keybinds.
GUI will be a separate crate, tiny will always be a minimal and compact TUI program. My work on this so far has mostly been to experiment with the UI API so that if people want a fancier TUI or a GUI it will be possible without changing tiny too much.
maybe trying to add a **modular** optional keys in ``config`` is better than force any preset
```
# Default
date-format: "%H:%M"
separator: ":"
line-output: "[date_format] [nick][separator] [message]"
```
One would be able to use
```
date-show: always
date-format: "%H%M%S"
separator: "❯"
line-output: "[date_format:7][nick:15] [separator] [message]"
```
Maybe BTW adding an option to set colors to date subelements in date-format
Another suggestion combining indentation with the current behaviour of `tiny` (time shown only when there is a change):
```
19:29     ahub: c'est quoi ton client eoli3n__ ?                                                                          
19:30 eoli3n__: https://github.com/osa1/tiny || tiny-irc-client-git AUR                                               
      eoli3n__: je viens de l'installer                                                                                     
          ahub: connais pas                                                                                                     
          ahub: c'est quoi la différence avec un weechat ou autre ?                                                             
      eoli3n__: je viens de decouvrir c'est top                                                                             
      eoli3n__: ahub un simple fichier de conf, pas besoin de plugin pour gérer les highlight et les
                notif                  
19:31     ahub: ho                                                                                                             
          ahub: ok :)                                                                                                     
eoli3n__: bonjour
```
Given that time and nicknames are in different colours, I believe the delimiter could be scraped, I find it minimal and easy to read currently already. There's still a lot of space lost because of right-aligned nicknames, and as stated in PR #207, long nicknames would be difficult to handle (column too wide, but also how to deal with them when they join an ongoing conversation?).

---

Another one, close to the current behaviour, would work for me too:
```
19:29 ahub: c'est quoi ton client eoli3n__ ?                                                                          
19:30 eoli3n__: https://github.com/osa1/tiny || tiny-irc-client-git AUR                                               
      eoli3n__: je viens de l'installer                                                                                     
      ahub: connais pas                                                                                                     
      ahub: c'est quoi la différence avec un weechat ou autre ?                                                             
      eoli3n__: je viens de decouvrir c'est top                                                                             
      eoli3n__: ahub un simple fichier de conf, pas besoin de plugin pour gérer les highlight et les
      notif                  
19:31 ahub: ho                                                                                                             
      ahub: ok :)                                                                                                     
eoli3n__: bonjour
```
That one just left aligns nicknames even when the timestamp is not shown, but otherwise keeps the current behaviour which better uses the available space. I think this would be my favourite.

---

Another idea suggested in PR #207 is to move time stamps to the right to avoid losing space on the left. Space is often available on the right since most messages are short:
```
ahub: c'est quoi ton client eoli3n__ ?                                  19:29                       
eoli3n__: https://github.com/osa1/tiny || tiny-irc-client-git AUR`      19:30                                                
eoli3n__: je viens de l'installer                                                                                     
ahub: connais pas                                                                                                     
ahub: c'est quoi la différence avec un weechat ou autre ?                                                             
eoli3n__: je viens de decouvrir c'est top                                                                            
eoli3n__: ahub un simple fichier de conf, pas besoin de plugin pour
gérer les highlight et les notif                  
ahub: ho                                                                19:31                   
ahub: ok :)                                                                                                     
eoli3n__: bonjour
```
I can see myself using that too.

Note that I don't dislike the current behaviour and wouldn't want it replaced, the minimal layout filling all available space is nice, but it takes some getting used to and configurable options would suit everyone's preferences.
what about just allowing a user-specified rust `fmt!` string with some known keys (user, timestamp, message, etc...)? instead of coming up with a bespoke format style?
@jedahan like https://github.com/osa1/tiny/issues/169#issuecomment-626142636
yeah just instead of making it multiple keys, just a single key, and using [the existing rust specification for format](https://doc.rust-lang.org/std/fmt/).

So instead of 
```
date-format: "%H:%M"
separator: ":"
line-output: "[date_format] [nick][separator] [message]" 
```

you would have something like
```
format: "{timestamp.hour:0>2}:{timestamp.minute:0>2} {nick}: {message}"
```

or if you wanted to right align nicks, say to maximum 16 chars:
```
format: "{nick:>16}: {message}"
```

Fix patch:
diff --git a/crates/libtiny_tui/src/config.rs b/crates/libtiny_tui/src/config.rs
index 337ac782..9f8b3e79 100644
--- a/crates/libtiny_tui/src/config.rs
+++ b/crates/libtiny_tui/src/config.rs
@@ -15,6 +15,7 @@ pub(crate) struct Config {
     pub(crate) colors: Colors,
     #[serde(default = "usize::max_value")]
     pub(crate) scrollback: usize,
+    pub(crate) ui_style: Option<UiStyle>,
 }
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -26,6 +27,13 @@ pub struct Style {
     pub bg: u16,
 }
 
+#[derive(Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub(crate) enum UiStyle {
+    Compact,
+    Aligned { max_nick_length: usize },
+}
+
 #[derive(Debug, Deserialize)]
 #[serde(default)]
 pub struct Colors {
diff --git a/crates/libtiny_tui/src/input_area/input_line.rs b/crates/libtiny_tui/src/input_area/input_line.rs
index 78f515a9..4a53456a 100644
--- a/crates/libtiny_tui/src/input_area/input_line.rs
+++ b/crates/libtiny_tui/src/input_area/input_line.rs
@@ -17,7 +17,7 @@ impl InputLine {
     pub(crate) fn new() -> InputLine {
         InputLine {
             buffer: Vec::with_capacity(512),
-            line_data: LineDataCache::new(true),
+            line_data: LineDataCache::input_line(0, 0),
         }
     }
 
@@ -25,7 +25,7 @@ impl InputLine {
     pub(crate) fn from_buffer(buffer: Vec<char>) -> InputLine {
         InputLine {
             buffer,
-            line_data: LineDataCache::new(true),
+            line_data: LineDataCache::input_line(0, 0),
         }
     }
 
@@ -84,8 +84,8 @@ impl InputLine {
     /// Calculate hedight 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 {
-        if self.line_data.is_dirty() || self.line_data.needs_resize(width, nick_length) {
-            self.line_data.reset(width, nick_length);
+        if self.line_data.is_dirty() || self.line_data.needs_resize(width, nick_length, None) {
+            self.line_data = LineDataCache::input_line(width, nick_length);
             self.line_data
                 .calculate_height(&mut self.buffer.iter().copied(), 0);
         }
diff --git a/crates/libtiny_tui/src/line_split.rs b/crates/libtiny_tui/src/line_split.rs
index c712a9bd..bb3ab0ed 100644
--- a/crates/libtiny_tui/src/line_split.rs
+++ b/crates/libtiny_tui/src/line_split.rs
@@ -1,82 +1,135 @@
 /// Cache that stores the state of a line's height calculation.
 /// `line_count` is used as the dirty bit to invalidate the cache.
 #[derive(Clone, Debug)]
-pub struct LineDataCache {
+pub(crate) struct LineDataCache {
     /// Indices to split on when we draw, not always whitespaces
     split_indices: Vec<i32>,
     /// The total number of lines (height) that will be rendered
     line_count: Option<i32>,
     /// The current width of InputArea. Used in determining if we need to invalidate due to resize.
     width: i32,
-    /// The current width of the InputLine (may be shorter due to nickname)
+    /// The width of the current line (full width minus nick_length or msg padding)
     line_width: i32,
-    /// Current nickname length. Used in determining if we need to invalidate due to resize.
-    nick_length: usize,
     /// The index into InputLine::buffer of the last whitespace that we saw in calculate_height()
     last_whitespace_idx: Option<i32>,
-    /// True if the last character was a whitespace character
-    prev_char_is_whitespace: bool,
     /// The length of the current line that is being added to.
     /// Used to determine when to wrap to the next line in calculate_height()
     current_line_length: i32,
-    /// If the line of text has a cursor
-    has_cursor: bool,
+    /// The type of line we're trying to calculate height for
+    line_type: LineType,
+}
+
+#[derive(Copy, Clone, Debug)]
+pub(crate) enum LineType {
+    Input {
+        /// Current nickname length. Used in determining if we need to invalidate due to resize.
+        nick_length: usize,
+    },
+    AlignedMsg {
+        /// The offset of timestamp + nick on multiline messages
+        msg_padding: usize,
+    },
+    /// A regular line
+    Msg,
+}
+
+impl LineType {
+    pub(crate) fn msg_padding(&self) -> Option<usize> {
+        if let LineType::AlignedMsg { msg_padding } = self {
+            Some(*msg_padding)
+        } else {
+            None
+        }
+    }
 }
 
 impl LineDataCache {
-    pub fn new(has_cursor: bool) -> LineDataCache {
+    pub(crate) fn input_line(width: i32, nick_length: usize) -> LineDataCache {
         LineDataCache {
             split_indices: Vec::new(),
             line_count: None,
-            width: 0,
-            line_width: 0,
-            nick_length: 0,
+            width,
+            line_width: width - nick_length as i32,
             last_whitespace_idx: None,
-            prev_char_is_whitespace: false,
             current_line_length: 0,
-            has_cursor,
+            line_type: LineType::Input { nick_length },
         }
     }
 
+    pub(crate) fn msg_line(width: i32, msg_padding: Option<usize>) -> LineDataCache {
+        let line_type = if let Some(msg_padding) = msg_padding {
+            LineType::AlignedMsg { msg_padding }
+        } else {
+            LineType::Msg
+        };
+        LineDataCache {
+            split_indices: Vec::new(),
+            line_count: None,
+            width,
+            line_width: width,
+            last_whitespace_idx: None,
+            current_line_length: 0,
+            line_type,
+        }
+    }
+
+    pub(crate) fn line_type(&self) -> LineType {
+        self.line_type
+    }
+
+    pub(crate) fn set_line_type(&mut self, line_type: LineType) {
+        self.line_type = line_type
+    }
+
     /// Performs a check to see if the width or nickname length changed
     /// which would require an invalidation of the cache and recalculation of
     /// the line height.
-    pub fn needs_resize(&self, width: i32, nick_length: usize) -> bool {
-        self.width != width || self.nick_length != nick_length
+    pub(crate) fn needs_resize(&self, width: i32, nick_len: usize, msg_pad: Option<usize>) -> bool {
+        // TODO so ugly
+        self.width != width
+            || match (self.line_type, msg_pad) {
+                (LineType::Input { nick_length }, ..) => nick_len != nick_length,
+                (LineType::AlignedMsg { msg_padding }, Some(pad)) => pad != msg_padding,
+                _ => false,
+            }
     }
 
     /// Sets `line_count` to `None`, which invalidates the cache.
-    pub fn set_dirty(&mut self) {
+    pub(crate) fn set_dirty(&mut self) {
         self.line_count = None;
     }
 
     /// Checks if the cache is invalidated by seeing if
     /// `line_count` is `None`.
-    pub fn is_dirty(&self) -> bool {
+    pub(crate) fn is_dirty(&self) -> bool {
         self.line_count.is_none()
     }
 
-    /// Resets the cache to a default state that requires
-    /// a height calculation.
-    pub fn reset(&mut self, width: i32, nick_length: usize) {
-        self.split_indices.clear();
-        self.line_count = None;
-        self.width = width;
-        self.nick_length = nick_length;
-        self.line_width = width - nick_length as i32;
-        self.last_whitespace_idx = None;
-        self.prev_char_is_whitespace = false;
-        self.current_line_length = 0;
-    }
-
-    pub fn get_line_count(&self) -> Option<usize> {
+    pub(crate) fn get_line_count(&self) -> Option<usize> {
         self.line_count.map(|c| c as usize)
     }
 
-    pub fn get_splits(&self) -> &[i32] {
+    pub(crate) fn get_splits(&self) -> &[i32] {
         &self.split_indices
     }
 
+    pub(crate) fn new_line_offset(&self) -> i32 {
+        match self.line_type {
+            LineType::Input { .. } | LineType::Msg => 0,
+            LineType::AlignedMsg { msg_padding } => msg_padding as i32,
+        }
+    }
+
+    /// Returns the width of lines that are not the first line
+    fn multi_line_width(&self) -> i32 {
+        match self.line_type {
+            // expand to full width
+            LineType::Input { .. } | LineType::Msg => self.width,
+            // consecutive lines can apply an offset
+            LineType::AlignedMsg { msg_padding } => self.width - msg_padding as i32,
+        }
+    }
+
     /// Function that calculates the height of the line.
     /// and sets `split_indices` for drawing.
     /// An `offset` allows for resuming the calculation - see InputLine::insert().
@@ -86,15 +139,18 @@ impl LineDataCache {
     /// will be needed to render the text with word wrapping.
     /// If an offset is provided, it will continue the calculation
     /// from the saved state and save the new line count in `line_count`.
-    pub fn calculate_height<I: Iterator<Item = char>>(&mut self, buffer: I, offset: usize) {
+    pub(crate) fn calculate_height<I: Iterator<Item = char>>(&mut self, buffer: I, offset: usize) {
         let mut temp_count = 1;
         if let Some(line_count) = self.line_count {
             temp_count = line_count;
             // If we made space for the cursor, subtract it.
-            if self.has_cursor && self.current_line_length == self.line_width {
-                temp_count -= 1;
+            if let LineType::Input { .. } = self.line_type {
+                if self.current_line_length == self.line_width {
+                    temp_count -= 1;
+                }
             }
         }
+
         for (c, current_idx) in buffer.skip(offset).zip(offset..) {
             let current_idx = current_idx as i32;
             self.current_line_length += 1;
@@ -108,24 +164,35 @@ impl LineDataCache {
                     self.current_line_length = 1;
                     // nick is shown on the first line, set width to full width in the consecutive
                     // lines
-                    self.line_width = self.width;
+                    self.line_width = self.multi_line_width();
+
                     // store index for drawing
                     self.split_indices.push(current_idx);
                 }
                 // store whitespace for splitting
                 self.last_whitespace_idx = Some(current_idx);
-                self.prev_char_is_whitespace = true;
             } else {
-                // Splitting
+                // Splitting on non-whitespace
                 if self.current_line_length > self.line_width {
+                    // set width to full width
+                    self.line_width = self.multi_line_width();
                     // if the previous character was a whitespace, then we have a clean split
-                    if !self.prev_char_is_whitespace && self.last_whitespace_idx.is_some() {
-                        // move back to the last whitespace and get the length of the input that
-                        // will be on the next line
-                        self.current_line_length = current_idx - self.last_whitespace_idx.unwrap();
-                        // store index for drawing
-                        self.split_indices
-                            .push(self.last_whitespace_idx.unwrap() + 1);
+                    if let Some(last_whitespace_idx) = self.last_whitespace_idx {
+                        // if the split is larger than the width we have,
+                        // we just want to do an unclean split (mainly only for links or if someone spams a super long line)
+                        if current_idx - last_whitespace_idx > self.line_width {
+                            // unclean split on non-whitespace
+                            self.current_line_length = 1;
+                            // store index for drawing
+                            self.split_indices.push(current_idx);
+                        } else {
+                            // move back to the last whitespace and get the length of the input that
+                            // will be on the next line
+                            self.current_line_length = current_idx - last_whitespace_idx;
+
+                            // store index for drawing
+                            self.split_indices.push(last_whitespace_idx + 1);
+                        }
                     } else {
                         // unclean split on non-whitespace
                         self.current_line_length = 1;
@@ -136,16 +203,15 @@ impl LineDataCache {
                     self.last_whitespace_idx = None;
                     // moved to next line
                     temp_count += 1;
-                    // set width to full width
-                    self.line_width = self.width;
                 }
-                self.prev_char_is_whitespace = false;
             }
         }
 
         // Last line length is `line_width`, make room for cursor
-        if self.has_cursor && self.current_line_length == self.line_width {
-            temp_count += 1;
+        if let LineType::Input { .. } = self.line_type {
+            if self.current_line_length == self.line_width {
+                temp_count += 1;
+            }
         }
         self.line_count = Some(temp_count);
     }
diff --git a/crates/libtiny_tui/src/messaging.rs b/crates/libtiny_tui/src/messaging.rs
index 4729d838..eeb29835 100644
--- a/crates/libtiny_tui/src/messaging.rs
+++ b/crates/libtiny_tui/src/messaging.rs
@@ -8,8 +8,8 @@ use time::{self, Tm};
 use crate::config::Colors;
 use crate::exit_dialogue::ExitDialogue;
 use crate::input_area::InputArea;
-use crate::msg_area::line::{SchemeStyle, SegStyle};
-use crate::msg_area::MsgArea;
+use crate::msg_area::line::SegStyle;
+use crate::msg_area::{Layout, MsgArea};
 use crate::trie::Trie;
 use crate::widget::WidgetRet;
 
@@ -46,9 +46,21 @@ pub(crate) struct Timestamp {
 }
 
 impl Timestamp {
+    /// The width of the timestamp plus a space
+    pub(crate) const WIDTH: usize = 6;
     fn stamp(self, msg_area: &mut MsgArea) {
-        msg_area.set_style(SegStyle::SchemeStyle(SchemeStyle::Timestamp));
-        msg_area.add_text(&format!("{:02}:{:02} ", self.hour, self.min));
+        msg_area.add_text(
+            &format!("{:02}:{:02} ", self.hour, self.min),
+            SegStyle::Timestamp,
+        );
+    }
+
+    /// Inserts a blank space that is the size of a timestamp
+    fn blank(msg_area: &mut MsgArea) {
+        msg_area.add_text(
+            &format!("{:^width$}", "", width = Timestamp::WIDTH),
+            SegStyle::Timestamp,
+        );
     }
 }
 
@@ -70,9 +82,15 @@ struct ActivityLine {
 }
 
 impl MessagingUI {
-    pub(crate) fn new(width: i32, height: i32, status: bool, scrollback: usize) -> MessagingUI {
+    pub(crate) fn new(
+        width: i32,
+        height: i32,
+        status: bool,
+        scrollback: usize,
+        msg_layout: Layout,
+    ) -> MessagingUI {
         MessagingUI {
-            msg_area: MsgArea::new(width, height - 1, scrollback),
+            msg_area: MsgArea::new(width, height - 1, scrollback, msg_layout),
             input_field: InputArea::new(width, get_input_field_max_height(height)),
             exit_dialogue: None,
             width,
@@ -232,6 +250,8 @@ impl MessagingUI {
         if let Some(ts_) = self.last_activity_ts {
             if ts_ != ts {
                 ts.stamp(&mut self.msg_area);
+            } else if matches!(self.msg_area.layout(), Layout::Aligned { .. }) {
+                Timestamp::blank(&mut self.msg_area)
             }
         } else {
             ts.stamp(&mut self.msg_area);
@@ -242,9 +262,7 @@ impl MessagingUI {
     pub(crate) fn show_topic(&mut self, topic: &str, ts: Timestamp) {
         self.add_timestamp(ts);
 
-        self.msg_area
-            .set_style(SegStyle::SchemeStyle(SchemeStyle::Topic));
-        self.msg_area.add_text(topic);
+        self.msg_area.add_text(topic, SegStyle::Topic);
 
         self.msg_area.flush_line();
     }
@@ -252,18 +270,14 @@ impl MessagingUI {
     pub(crate) fn add_client_err_msg(&mut self, msg: &str) {
         self.reset_activity_line();
 
-        self.msg_area
-            .set_style(SegStyle::SchemeStyle(SchemeStyle::ErrMsg));
-        self.msg_area.add_text(msg);
+        self.msg_area.add_text(msg, SegStyle::ErrMsg);
         self.msg_area.flush_line();
     }
 
     pub(crate) fn add_client_notify_msg(&mut self, msg: &str) {
         self.reset_activity_line();
 
-        self.msg_area
-            .set_style(SegStyle::SchemeStyle(SchemeStyle::Faded));
-        self.msg_area.add_text(msg);
+        self.msg_area.add_text(msg, SegStyle::Faded);
         self.msg_area.flush_line();
         self.reset_activity_line();
     }
@@ -271,9 +285,7 @@ impl MessagingUI {
     pub(crate) fn add_client_msg(&mut self, msg: &str) {
         self.reset_activity_line();
 
-        self.msg_area
-            .set_style(SegStyle::SchemeStyle(SchemeStyle::UserMsg));
-        self.msg_area.add_text(msg);
+        self.msg_area.add_text(msg, SegStyle::UserMsg);
         self.msg_area.flush_line();
         self.reset_activity_line();
     }
@@ -295,33 +307,45 @@ impl MessagingUI {
         self.reset_activity_line();
         self.add_timestamp(ts);
 
+        let nick_color = self.get_nick_color(sender);
+        let nick_col_style = SegStyle::NickColor(nick_color);
+
+        // actions are /me msgs so they don't show the nick in the nick column, but in the msg
+        let layout = self.msg_area.layout();
+        let format_nick = |s: &str| -> String {
+            if let Layout::Aligned { max_nick_len, .. } = layout {
+                let mut aligned = format!("{:>padding$.padding$}", s, padding = max_nick_len);
+                if s.len() > max_nick_len {
+                    aligned.pop();
+                    aligned.push('…');
+                }
+                aligned
+            } else {
+                s.to_string()
+            }
+        };
         if is_action {
             self.msg_area
-                .set_style(SegStyle::SchemeStyle(SchemeStyle::UserMsg));
-            self.msg_area.add_text("** ");
-        }
-
-        {
-            let nick_color = self.get_nick_color(sender);
-            let style = SegStyle::Index(nick_color);
-            self.msg_area.set_style(style);
-            self.msg_area.add_text(sender);
-        }
-
-        self.msg_area
-            .set_style(SegStyle::SchemeStyle(SchemeStyle::UserMsg));
-
-        if !is_action {
-            self.msg_area.add_char(':');
+                .add_text(&format_nick("**"), SegStyle::UserMsg);
+            // separator between nick and msg
+            self.msg_area.add_text("  ", SegStyle::Faded);
+            self.msg_area.add_text(sender, nick_col_style);
+            // a space replacing the usual ':'
+            self.msg_area.add_text(" ", SegStyle::UserMsg);
+        } else {
+            self.msg_area.add_text(&format_nick(sender), nick_col_style);
+            // separator between nick and msg
+            self.msg_area.add_text(": ", SegStyle::Faded);
         }
-        self.msg_area.add_char(' ');
 
-        if highlight {
-            self.msg_area
-                .set_style(SegStyle::SchemeStyle(SchemeStyle::Highlight));
-        }
+        let msg_style = if highlight {
+            SegStyle::Highlight
+        } else {
+            SegStyle::UserMsg
+        };
 
-        self.msg_area.add_text(msg);
+        self.msg_area.add_text(msg, msg_style);
+        self.msg_area.set_current_line_alignment();
         self.msg_area.flush_line();
     }
 
@@ -329,9 +353,7 @@ impl MessagingUI {
         self.reset_activity_line();
 
         self.add_timestamp(ts);
-        self.msg_area
-            .set_style(SegStyle::SchemeStyle(SchemeStyle::UserMsg));
-        self.msg_area.add_text(msg);
+        self.msg_area.add_text(msg, SegStyle::UserMsg);
         self.msg_area.flush_line();
     }
 
@@ -339,9 +361,7 @@ impl MessagingUI {
         self.reset_activity_line();
 
         self.add_timestamp(ts);
-        self.msg_area
-            .set_style(SegStyle::SchemeStyle(SchemeStyle::ErrMsg));
-        self.msg_area.add_text(msg);
+        self.msg_area.add_text(msg, SegStyle::ErrMsg);
         self.msg_area.flush_line();
     }
 
@@ -372,10 +392,8 @@ impl MessagingUI {
             if let Some(ts) = ts {
                 let line_idx = self.get_activity_line_idx(ts);
                 self.msg_area.modify_line(line_idx, |line| {
-                    line.set_style(SegStyle::SchemeStyle(SchemeStyle::Join));
-                    line.add_char('+');
-                    line.set_style(SegStyle::SchemeStyle(SchemeStyle::Faded));
-                    line.add_text(nick);
+                    line.add_char('+', SegStyle::Join);
+                    line.add_text(nick, SegStyle::Faded);
                 });
             }
         }
@@ -390,10 +408,8 @@ impl MessagingUI {
             if let Some(ts) = ts {
                 let line_idx = self.get_activity_line_idx(ts);
                 self.msg_area.modify_line(line_idx, |line| {
-                    line.set_style(SegStyle::SchemeStyle(SchemeStyle::Part));
-                    line.add_char('-');
-                    line.set_style(SegStyle::SchemeStyle(SchemeStyle::Faded));
-                    line.add_text(nick);
+                    line.add_char('-', SegStyle::Part);
+                    line.add_text(nick, SegStyle::Faded);
                 });
             }
         }
@@ -420,12 +436,9 @@ impl MessagingUI {
 
         let line_idx = self.get_activity_line_idx(ts);
         self.msg_area.modify_line(line_idx, |line| {
-            line.set_style(SegStyle::SchemeStyle(SchemeStyle::Faded));
-            line.add_text(old_nick);
-            line.set_style(SegStyle::SchemeStyle(SchemeStyle::Nick));
-            line.add_char('>');
-            line.set_style(SegStyle::SchemeStyle(SchemeStyle::Faded));
-            line.add_text(new_nick);
+            line.add_text(old_nick, SegStyle::Faded);
+            line.add_char('>', SegStyle::Nick);
+            line.add_text(new_nick, SegStyle::Faded);
         });
     }
 
@@ -443,11 +456,23 @@ impl MessagingUI {
                 // to avoid adding redundant spaces. The test `small_screen_1` breaks if we don't
                 // get this right.
                 self.msg_area
-                    .modify_line(line_idx, |line| line.add_char(' '));
+                    .modify_line(line_idx, |line| line.add_char(' ', SegStyle::UserMsg));
                 line_idx
             }
             _ => {
                 self.add_timestamp(ts);
+                if let Layout::Aligned {
+                    max_nick_len,
+                    msg_nick_sep_len,
+                    ..
+                } = self.msg_area.layout()
+                {
+                    self.msg_area.add_text(
+                        &format!("{:^max$}", "", max = max_nick_len + msg_nick_sep_len),
+                        SegStyle::UserMsg,
+                    )
+                }
+                self.msg_area.set_current_line_alignment();
                 let line_idx = self.msg_area.flush_line();
                 self.last_activity_line = Some(ActivityLine { ts, line_idx });
                 line_idx
diff --git a/crates/libtiny_tui/src/msg_area/line.rs b/crates/libtiny_tui/src/msg_area/line.rs
index 2e4a9f39..9568eea5 100644
--- a/crates/libtiny_tui/src/msg_area/line.rs
+++ b/crates/libtiny_tui/src/msg_area/line.rs
@@ -1,5 +1,8 @@
+use crate::line_split::LineType;
 use crate::{
-    config, config::Colors, line_split::LineDataCache, utils::translate_irc_control_chars,
+    config::{Colors, Style},
+    line_split::LineDataCache,
+    utils::translate_irc_control_chars,
 };
 use std::mem;
 use termbox_simple::{self, Termbox};
@@ -9,14 +12,10 @@ use termbox_simple::{self, Termbox};
 #[derive(Debug)]
 pub(crate) struct Line {
     /// Line segments.
-    string_segments: Vec<StyledString>,
-
+    segments: Vec<StyledString>,
     /// The segment we're currently extending.
     current_seg: StyledString,
 
-    /// Number of characters in the line (includes all segments).
-    len_chars: i32,
-
     line_data: LineDataCache,
 }
 
@@ -30,18 +29,13 @@ struct StyledString {
 pub(crate) enum SegStyle {
     /// A specific style. Useful when rendering IRC colors (which should look
     /// the same across color schemes).
-    Fixed(config::Style),
+    Fixed(Style),
 
     /// An index to nick colors. Note that the index should be larger than size
     /// of the color list, so make sure to use mod.
-    Index(usize),
+    NickColor(usize),
 
     /// A style from the current color scheme.
-    SchemeStyle(SchemeStyle),
-}
-
-#[derive(Debug, PartialEq, Eq, Copy, Clone)]
-pub(crate) enum SchemeStyle {
     UserMsg,
     ErrMsg,
     Topic,
@@ -54,27 +48,32 @@ pub(crate) enum SchemeStyle {
 }
 
 impl StyledString {
-    pub(crate) fn style(&self, colors: &Colors) -> config::Style {
+    pub(crate) fn style(&self, colors: &Colors) -> Style {
+        use SegStyle::*;
         match self.style {
-            SegStyle::Fixed(style) => style,
-            SegStyle::Index(idx) => config::Style {
+            Fixed(style) => style,
+            NickColor(idx) => Style {
                 fg: u16::from(colors.nick[idx % colors.nick.len()]),
                 bg: colors.user_msg.bg,
             },
-            SegStyle::SchemeStyle(sty) => {
-                use self::SchemeStyle::*;
-                match sty {
-                    UserMsg => colors.user_msg,
-                    ErrMsg => colors.err_msg,
-                    Topic => colors.topic,
-                    Join => colors.join,
-                    Part => colors.part,
-                    Nick => colors.nick_change,
-                    Faded => colors.faded,
-                    Highlight => colors.highlight,
-                    Timestamp => colors.timestamp,
-                }
-            }
+            UserMsg => colors.user_msg,
+            ErrMsg => colors.err_msg,
+            Topic => colors.topic,
+            Join => colors.join,
+            Part => colors.part,
+            Nick => colors.nick_change,
+            Faded => colors.faded,
+            Highlight => colors.highlight,
+            Timestamp => colors.timestamp,
+        }
+    }
+}
+
+impl Default for StyledString {
+    fn default() -> Self {
+        StyledString {
+            string: String::new(),
+            style: SegStyle::UserMsg,
         }
     }
 }
@@ -85,17 +84,21 @@ const TERMBOX_COLOR_PREFIX: char = '\x00';
 impl Line {
     pub(crate) fn new() -> Line {
         Line {
-            string_segments: vec![],
-            current_seg: StyledString {
-                string: String::new(),
-                style: SegStyle::SchemeStyle(SchemeStyle::UserMsg),
-            },
-            len_chars: 0,
-            line_data: LineDataCache::new(false),
+            segments: vec![],
+            current_seg: StyledString::default(),
+            line_data: LineDataCache::msg_line(0, None),
         }
     }
 
-    pub(crate) fn set_style(&mut self, style: SegStyle) {
+    pub(crate) fn set_type(&mut self, line_type: LineType) {
+        self.line_data.set_line_type(line_type)
+    }
+
+    pub(crate) fn line_type(&self) -> LineType {
+        self.line_data.line_type()
+    }
+
+    fn set_message_style(&mut self, style: SegStyle) {
         // Just update the last segment if it's empty
         if self.current_seg.string.is_empty() {
             self.current_seg.style = style;
@@ -107,11 +110,11 @@ impl Line {
                     style,
                 },
             );
-            self.string_segments.push(seg);
+            self.segments.push(seg);
         }
     }
 
-    pub(crate) fn add_text(&mut self, str: &str) {
+    fn add_text_inner(&mut self, str: &str) {
         fn push_color(ret: &mut String, irc_fg: u8, irc_bg: Option<u8>) {
             ret.push(TERMBOX_COLOR_PREFIX);
             ret.push(0 as char); // style
@@ -133,20 +136,23 @@ impl Line {
                 let bg = iter.next().unwrap() as u8;
                 let fg = (u16::from(st) << 8) | u16::from(fg);
                 let bg = u16::from(bg);
-                let style = config::Style { fg, bg };
-                self.set_style(SegStyle::Fixed(style));
+                let style = Style { fg, bg };
+                self.set_message_style(SegStyle::Fixed(style));
             } else if char > '\x08' {
                 self.current_seg.string.push(char);
-                self.len_chars += 1;
             }
         }
     }
 
-    pub(crate) fn add_char(&mut self, char: char) {
-        assert_ne!(char, TERMBOX_COLOR_PREFIX);
+    pub(crate) fn add_text(&mut self, str: &str, style: SegStyle) {
+        self.set_message_style(style);
+        self.add_text_inner(str)
+    }
 
+    pub(crate) fn add_char(&mut self, char: char, style: SegStyle) {
+        assert_ne!(char, TERMBOX_COLOR_PREFIX);
+        self.set_message_style(style);
         self.current_seg.string.push(char);
-        self.len_chars += 1;
     }
 
     pub(crate) fn force_recalculation(&mut self) {
@@ -156,11 +162,11 @@ impl Line {
     /// Calculates the number of lines that this line will be.
     /// The calculation is only done if the line_data is dirty or the window is resized.
     pub(crate) fn rendered_height(&mut self, width: i32) -> i32 {
-        if self.line_data.is_dirty() || self.line_data.needs_resize(width, 0) {
-            self.line_data.reset(width, 0);
-            // Get all segs together
+        let msg_padding = self.line_type().msg_padding();
+        if self.line_data.is_dirty() || self.line_data.needs_resize(width, 0, msg_padding) {
+            self.line_data = LineDataCache::msg_line(width, msg_padding);
             let mut full_line = self
-                .string_segments
+                .segments
                 .iter()
                 .flat_map(|s| s.string.chars())
                 .chain(self.current_seg.string.chars());
@@ -184,7 +190,7 @@ impl Line {
         let mut split_indices_iter = self.line_data.get_splits().iter().copied().peekable();
 
         for seg in self
-            .string_segments
+            .segments
             .iter()
             .chain(std::iter::once(&self.current_seg))
         {
@@ -200,7 +206,8 @@ impl Line {
                             break;
                         }
                         // Reset column
-                        col = 0;
+                        col = pos_x + self.line_data.new_line_offset();
+
                         // Move to the next line start index
                         split_indices_iter.next();
                         // Don't draw whitespaces
@@ -259,7 +266,7 @@ mod tests {
     #[test]
     fn height_test_1() {
         let mut line = Line::new();
-        line.add_text("a b c d e");
+        line.add_text("a b c d e", SegStyle::UserMsg);
         assert_eq!(line.rendered_height(1), 9);
         assert_eq!(line.rendered_height(2), 5);
         assert_eq!(line.rendered_height(3), 3);
@@ -274,7 +281,7 @@ mod tests {
     #[test]
     fn height_test_2() {
         let mut line = Line::new();
-        line.add_text("ab c d e");
+        line.add_text("ab c d e", SegStyle::UserMsg);
         assert_eq!(line.rendered_height(1), 8);
         assert_eq!(line.rendered_height(2), 4);
         assert_eq!(line.rendered_height(3), 3);
@@ -288,7 +295,7 @@ mod tests {
     #[test]
     fn height_test_3() {
         let mut line = Line::new();
-        line.add_text("ab cd e");
+        line.add_text("ab cd e", SegStyle::UserMsg);
         assert_eq!(line.rendered_height(1), 7);
         assert_eq!(line.rendered_height(2), 4);
         assert_eq!(line.rendered_height(3), 3);
@@ -301,7 +308,7 @@ mod tests {
     #[test]
     fn height_test_4() {
         let mut line = Line::new();
-        line.add_text("ab cde");
+        line.add_text("ab cde", SegStyle::UserMsg);
         assert_eq!(line.rendered_height(1), 6);
         assert_eq!(line.rendered_height(2), 4);
         assert_eq!(line.rendered_height(3), 2);
@@ -329,9 +336,24 @@ mod tests {
         };
 
         let mut line = Line::new();
-        line.add_text(&text);
+        line.add_text(&text, SegStyle::UserMsg);
         // lipsum.txt has 1160 words in it. each line should contain at most one
         // word so we should have 1160 lines.
         assert_eq!(line.rendered_height(80), 102);
     }
+
+    #[test]
+    fn align_test() {
+        let mut line = Line::new();
+        line.set_type(LineType::AlignedMsg { msg_padding: 1 });
+        /*
+        123
+         45
+         67
+         8
+        */
+        line.add_text_inner("12345678");
+
+        assert_eq!(line.rendered_height(3), 4);
+    }
 } // mod tests
diff --git a/crates/libtiny_tui/src/msg_area/mod.rs b/crates/libtiny_tui/src/msg_area/mod.rs
index 4fc9786c..ecb8a676 100644
--- a/crates/libtiny_tui/src/msg_area/mod.rs
+++ b/crates/libtiny_tui/src/msg_area/mod.rs
@@ -5,7 +5,9 @@ use std::{cmp::max, mem, str};
 use termbox_simple::Termbox;
 
 pub(crate) use self::line::{Line, SegStyle};
-use crate::config::Colors;
+use crate::config::{Colors, UiStyle};
+use crate::line_split::LineType;
+use crate::messaging::Timestamp;
 
 pub(crate) struct MsgArea {
     lines: VecDeque<Line>,
@@ -25,10 +27,49 @@ pub(crate) struct MsgArea {
     /// Cached total rendered height of all lines. Invalidate on resize, update
     /// when adding new lines.
     lines_height: Option<i32>,
+
+    layout: Layout,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub(crate) enum Layout {
+    Compact,
+    Aligned {
+        /// length of timestamp + a space
+        timestamp_len: usize,
+        max_nick_len: usize,
+        msg_nick_sep_len: usize,
+    },
+}
+
+impl Layout {
+    fn msg_padding(&self) -> usize {
+        match self {
+            Layout::Compact => 0,
+            Layout::Aligned {
+                timestamp_len,
+                max_nick_len,
+                msg_nick_sep_len,
+            } => timestamp_len + max_nick_len + msg_nick_sep_len,
+        }
+    }
+}
+
+impl From<UiStyle> for Layout {
+    fn from(ui_style: UiStyle) -> Self {
+        match ui_style {
+            UiStyle::Compact => Layout::Compact,
+            UiStyle::Aligned { max_nick_length } => Layout::Aligned {
+                timestamp_len: Timestamp::WIDTH,
+                max_nick_len: max_nick_length,
+                msg_nick_sep_len: 2,
+            },
+        }
+    }
 }
 
 impl MsgArea {
-    pub(crate) fn new(width: i32, height: i32, scrollback: usize) -> MsgArea {
+    pub(crate) fn new(width: i32, height: i32, scrollback: usize, layout: Layout) -> MsgArea {
         MsgArea {
             lines: VecDeque::with_capacity(512.min(scrollback)),
             scrollback,
@@ -37,6 +78,7 @@ impl MsgArea {
             scroll: 0,
             line_buf: Line::new(),
             lines_height: Some(0),
+            layout,
         }
     }
 
@@ -50,6 +92,16 @@ impl MsgArea {
         self.lines_height = None;
     }
 
+    pub(crate) fn layout(&self) -> Layout {
+        self.layout
+    }
+
+    /// Used to force a line to be aligned.
+    pub(crate) fn set_current_line_alignment(&mut self) {
+        let msg_padding = self.layout.msg_padding();
+        self.line_buf.set_type(LineType::AlignedMsg { msg_padding });
+    }
+
     pub(crate) fn draw(&mut self, tb: &mut Termbox, colors: &Colors, pos_x: i32, pos_y: i32) {
         // Where to render current line
         let mut row = pos_y + self.height - 1;
@@ -143,18 +195,9 @@ impl MsgArea {
 
 ////////////////////////////////////////////////////////////////////////////////
 // Adding/removing text
-
 impl MsgArea {
-    pub(crate) fn set_style(&mut self, style: SegStyle) {
-        self.line_buf.set_style(style);
-    }
-
-    pub(crate) fn add_text(&mut self, str: &str) {
-        self.line_buf.add_text(str);
-    }
-
-    pub(crate) fn add_char(&mut self, char: char) {
-        self.line_buf.add_char(char);
+    pub(crate) fn add_text(&mut self, str: &str, style: SegStyle) {
+        self.line_buf.add_text(str, style);
     }
 
     pub(crate) fn flush_line(&mut self) -> usize {
@@ -201,20 +244,20 @@ mod tests {
 
     #[test]
     fn newline_scrolling() {
-        let mut msg_area = MsgArea::new(100, 1, usize::MAX);
+        let mut msg_area = MsgArea::new(100, 1, usize::MAX, Layout::Compact);
         // Adding a new line when scroll is 0 should not change it
         assert_eq!(msg_area.scroll, 0);
-        msg_area.add_text("line1");
+        msg_area.add_text("line1", SegStyle::UserMsg);
         msg_area.flush_line();
         assert_eq!(msg_area.scroll, 0);
 
-        msg_area.add_text("line2");
+        msg_area.add_text("line2", SegStyle::UserMsg);
         msg_area.flush_line();
         assert_eq!(msg_area.scroll, 0);
 
         msg_area.scroll_up();
         assert_eq!(msg_area.scroll, 1);
-        msg_area.add_text("line3");
+        msg_area.add_text("line3", SegStyle::UserMsg);
         msg_area.flush_line();
         assert_eq!(msg_area.scroll, 2);
     }
@@ -222,15 +265,15 @@ mod tests {
     #[test]
     fn test_max_lines() {
         // Can't show more than 3 lines.
-        let mut msg_area = MsgArea::new(100, 1, 3);
-        msg_area.add_text("first");
+        let mut msg_area = MsgArea::new(100, 1, 3, Layout::Compact);
+        msg_area.add_text("first", SegStyle::UserMsg);
         msg_area.flush_line();
-        msg_area.add_text("second");
+        msg_area.add_text("second", SegStyle::UserMsg);
         msg_area.flush_line();
-        msg_area.add_text("third");
+        msg_area.add_text("third", SegStyle::UserMsg);
         msg_area.flush_line();
         assert_eq!(msg_area.lines.len(), 3);
-        msg_area.add_text("fourth");
+        msg_area.add_text("fourth", SegStyle::UserMsg);
         // Will pop out "first" line
         msg_area.flush_line();
         assert_eq!(msg_area.lines.len(), 3);
diff --git a/crates/libtiny_tui/src/tui.rs b/crates/libtiny_tui/src/tui.rs
index 0a4b4efc..349390f8 100644
--- a/crates/libtiny_tui/src/tui.rs
+++ b/crates/libtiny_tui/src/tui.rs
@@ -11,6 +11,7 @@ use time::Tm;
 use crate::config::{parse_config, Colors, Config, Style};
 use crate::editor;
 use crate::messaging::{MessagingUI, Timestamp};
+use crate::msg_area::Layout;
 use crate::notifier::Notifier;
 use crate::tab::Tab;
 use crate::widget::WidgetRet;
@@ -76,6 +77,9 @@ pub struct TUI {
     /// Max number of message lines
     scrollback: usize,
 
+    /// Messaging layout
+    msg_layout: Layout,
+
     tabs: Vec<Tab>,
     active_idx: usize,
     width: i32,
@@ -108,6 +112,11 @@ impl TUI {
         self.tb.activate()
     }
 
+    #[cfg(test)]
+    pub(crate) fn set_layout(&mut self, layout: Layout) {
+        self.msg_layout = layout
+    }
+
     fn new_tb(config_path: Option<PathBuf>, tb: Termbox) -> TUI {
         // This is now done in reload_config() below
         // tb.set_clear_attributes(colors.clear.fg as u8, colors.clear.bg as u8);
@@ -119,6 +128,7 @@ impl TUI {
             tb,
             colors: Colors::default(),
             scrollback: usize::MAX,
+            msg_layout: Layout::Compact,
             tabs: Vec::new(),
             active_idx: 0,
             width,
@@ -269,9 +279,16 @@ impl TUI {
                         &MsgTarget::CurrentTab,
                     );
                 }
-                Ok(Config { colors, scrollback }) => {
+                Ok(Config {
+                    colors,
+                    scrollback,
+                    ui_style,
+                }) => {
                     self.set_colors(colors);
                     self.scrollback = scrollback.max(1);
+                    if let Some(ui_style) = ui_style {
+                        self.msg_layout = ui_style.into()
+                    }
                 }
             }
         }
@@ -331,7 +348,13 @@ impl TUI {
             idx,
             Tab {
                 alias,
-                widget: MessagingUI::new(self.width, self.height - 1, status, self.scrollback),
+                widget: MessagingUI::new(
+                    self.width,
+                    self.height - 1,
+                    status,
+                    self.scrollback,
+                    self.msg_layout,
+                ),
                 src,
                 style: TabStyle::Normal,
                 switch,
diff --git a/crates/tiny/config.yml b/crates/tiny/config.yml
index 30553170..4d870aed 100644
--- a/crates/tiny/config.yml
+++ b/crates/tiny/config.yml
@@ -11,7 +11,7 @@ servers:
 
       # Channels to automatically join
       join:
-          - '#tiny'
+          - "#tiny"
 
       # Three authentication methods: pass, sasl, and nickserv_ident
       # These are optional and you probably only need one of these, delete
@@ -37,11 +37,18 @@ defaults:
     tls: false
 
 # Where to put log files
-log_dir: '{}'
+log_dir: "{}"
 
 # (Optional) Limits the maximum number of messages stored for each UI tab. Defaults to unlimited.
 # scrollback: 512
 
+# (Optional) UI Message Layout Style. Defaults to 'compact'
+# ui_style: compact
+# ui_style:
+#   aligned:
+#     # Nicks greater than this value will be truncated
+#     max_nick_length: 12
+
 # Color theme based on 256 colors. Colors can be defined as color indices
 # (0-255) or with their names.
 #
@@ -56,7 +63,7 @@ log_dir: '{}'
 # Attributes can be combined (e.g [bold, underline]), and valid values are bold
 # and underline.
 colors:
-    nick: [ 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14 ]
+    nick: [1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14]
 
     clear:
         fg: default


Test patch:
diff --git a/crates/libtiny_tui/src/tests/mod.rs b/crates/libtiny_tui/src/tests/mod.rs
index 499dc910..9002c59a 100644
--- a/crates/libtiny_tui/src/tests/mod.rs
+++ b/crates/libtiny_tui/src/tests/mod.rs
@@ -1,3 +1,4 @@
+use crate::msg_area::Layout;
 use crate::tui::TUI;
 
 use crate::test_utils::expect_screen;
@@ -408,6 +409,44 @@ fn test_join_part_overflow() {
     expect_screen(screen, &tui.get_front_buffer(), 21, 4, Location::caller());
 }
 
+#[test]
+fn test_alignment_long_string() {
+    let mut tui = TUI::new_test(40, 5);
+    tui.set_layout(Layout::Aligned {
+        timestamp_len: 6,
+        max_nick_len: 12,
+        msg_nick_sep_len: 2,
+    });
+    let serv = "irc.server_1.org";
+    let chan = ChanNameRef::new("#chan");
+    tui.new_server_tab(serv, None);
+    tui.set_nick(serv, "osa1");
+    tui.new_chan_tab(serv, chan);
+    tui.next_tab();
+    tui.next_tab();
+
+    let target = MsgTarget::Chan { serv, chan };
+    let ts = time::at_utc(time::Timespec::new(0, 0));
+    tui.add_privmsg(
+        "osa1",
+        "123456789012345678901234567890",
+        ts,
+        &target,
+        false,
+        false,
+    );
+    tui.draw();
+
+    #[rustfmt::skip]
+    let screen =
+        "|                                        |
+         |00:00         osa1: 12345678901234567890|
+         |                    1234567890          |
+         |osa1:                                   |
+         |mentions irc.server_1.org #chan         |";
+
+    expect_screen(screen, &tui.get_front_buffer(), 40, 5, Location::caller());
+}
 #[test]
 fn test_resize() {
     let mut tui = TUI::new_test(80, 50);