diff --git a/BAN/include/BAN/Debug.h b/BAN/include/BAN/Debug.h
index c0fd529a43..2c9e656c3f 100644
--- a/BAN/include/BAN/Debug.h
+++ b/BAN/include/BAN/Debug.h
@@ -14,7 +14,7 @@
 #define dprintln(...)											\
 	do {														\
 		BAN::Formatter::print(__debug_putchar, __VA_ARGS__);	\
-		BAN::Formatter::print(__debug_putchar,"\r\n");			\
+		BAN::Formatter::print(__debug_putchar,"\n");			\
 		fflush(stddbg);											\
 	} while (false)
 
@@ -22,7 +22,7 @@
 	do {														\
 		BAN::Formatter::print(__debug_putchar, "\e[33m");		\
 		BAN::Formatter::print(__debug_putchar, __VA_ARGS__);	\
-		BAN::Formatter::print(__debug_putchar, "\e[m\r\n");		\
+		BAN::Formatter::print(__debug_putchar, "\e[m\n");		\
 		fflush(stddbg);											\
 	} while(false)
 
@@ -30,7 +30,7 @@
 	do {														\
 		BAN::Formatter::print(__debug_putchar, "\e[31m");		\
 		BAN::Formatter::print(__debug_putchar, __VA_ARGS__);	\
-		BAN::Formatter::print(__debug_putchar, "\e[m\r\n");		\
+		BAN::Formatter::print(__debug_putchar, "\e[m\n");		\
 		fflush(stddbg);											\
 	} while(false)
 
diff --git a/kernel/include/kernel/Terminal/TTY.h b/kernel/include/kernel/Terminal/TTY.h
index f1078ab99e..77ba288cb2 100644
--- a/kernel/include/kernel/Terminal/TTY.h
+++ b/kernel/include/kernel/Terminal/TTY.h
@@ -57,7 +57,7 @@ namespace Kernel
 		virtual bool has_hungup_impl() const override { return false; }
 
 	protected:
-		TTY(mode_t mode, uid_t uid, gid_t gid);
+		TTY(termios termios, mode_t mode, uid_t uid, gid_t gid);
 
 		virtual bool putchar_impl(uint8_t ch) = 0;
 		virtual void update_cursor() {}
diff --git a/kernel/kernel/Terminal/PseudoTerminal.cpp b/kernel/kernel/Terminal/PseudoTerminal.cpp
index 0e4e463296..79f71c1744 100644
--- a/kernel/kernel/Terminal/PseudoTerminal.cpp
+++ b/kernel/kernel/Terminal/PseudoTerminal.cpp
@@ -158,7 +158,15 @@ namespace Kernel
 	}
 
 	PseudoTerminalSlave::PseudoTerminalSlave(BAN::String&& name, uint32_t number, mode_t mode, uid_t uid, gid_t gid)
-		: TTY(mode, uid, gid)
+		: TTY({
+			.c_iflag = 0,
+			.c_oflag = 0,
+			.c_cflag = CS8,
+			.c_lflag = ECHO | ICANON,
+			.c_cc = {},
+			.c_ospeed = B38400,
+			.c_ispeed = B38400,
+		  }, mode, uid, gid)
 		, m_name(BAN::move(name))
 		, m_number(number)
 	{}
diff --git a/kernel/kernel/Terminal/Serial.cpp b/kernel/kernel/Terminal/Serial.cpp
index bcc68831eb..b6bec3d9c2 100644
--- a/kernel/kernel/Terminal/Serial.cpp
+++ b/kernel/kernel/Terminal/Serial.cpp
@@ -170,7 +170,15 @@ namespace Kernel
 	}
 
 	SerialTTY::SerialTTY(Serial serial)
-		: TTY(0600, 0, 0)
+		: TTY({
+			.c_iflag = ICRNL,
+			.c_oflag = OPOST | ONLCR,
+			.c_cflag = CS8,
+			.c_lflag = ECHO | ICANON,
+			.c_cc = {},
+			.c_ospeed = B38400,
+			.c_ispeed = B38400,
+		  }, 0600, 0, 0)
 		, m_name(MUST(BAN::String::formatted("ttyS{}", s_next_tty_number++)))
 		, m_serial(serial)
 	{}
@@ -235,8 +243,6 @@ namespace Kernel
 			while (!m_input.empty())
 			{
 				*ptr = m_input.front();
-				if (*ptr == '\r')
-					*ptr = '\n';
 				m_input.pop();
 				ptr++;
 			}
diff --git a/kernel/kernel/Terminal/TTY.cpp b/kernel/kernel/Terminal/TTY.cpp
index 230376b187..33f7c94871 100644
--- a/kernel/kernel/Terminal/TTY.cpp
+++ b/kernel/kernel/Terminal/TTY.cpp
@@ -66,18 +66,11 @@ namespace Kernel
 		return makedev(DeviceNumber::TTY, s_minor++);
 	}
 
-	TTY::TTY(mode_t mode, uid_t uid, gid_t gid)
+	TTY::TTY(termios termios, mode_t mode, uid_t uid, gid_t gid)
 		: CharacterDevice(mode, uid, gid)
+		, m_termios(termios)
 		, m_rdev(next_tty_rdev())
-	{
-		// FIXME: add correct baud and flags
-		m_termios.c_iflag = 0;
-		m_termios.c_oflag = 0;
-		m_termios.c_cflag = CS8;
-		m_termios.c_lflag = ECHO | ICANON;
-		m_termios.c_ospeed = B38400;
-		m_termios.c_ispeed = B38400;
-	}
+	{ }
 
 	BAN::RefPtr<TTY> TTY::current()
 	{
@@ -228,6 +221,17 @@ namespace Kernel
 
 		LockGuard _(m_mutex);
 
+		if ((m_termios.c_iflag & ISTRIP))
+			ch &= 0x7F;
+		if ((m_termios.c_iflag & IGNCR) && ch == '\r')
+			return;
+		uint8_t conv = ch;
+		if ((m_termios.c_iflag & ICRNL) && ch == '\r')
+			conv = '\n';
+		if ((m_termios.c_iflag & INLCR) && ch == '\n')
+			conv = '\r';
+		ch = conv;
+
 		// ^C
 		if (ch == '\x03')
 		{
@@ -252,6 +256,10 @@ namespace Kernel
 			return;
 		}
 
+		// FIXME: don't ignore these bytes
+		if (m_output.bytes >= m_output.buffer.size())
+			return;
+
 		m_output.buffer[m_output.bytes++] = ch;
 
 		if (m_termios.c_lflag & ECHO)
@@ -280,7 +288,7 @@ namespace Kernel
 			}
 		}
 
-		if (ch == '\n' || !(m_termios.c_lflag & ICANON))
+		if (ch == '\n' || !(m_termios.c_lflag & ICANON) || m_output.bytes == m_output.buffer.size())
 		{
 			m_output.flush = true;
 			epoll_notify(EPOLLIN);
@@ -334,9 +342,16 @@ namespace Kernel
 	bool TTY::putchar(uint8_t ch)
 	{
 		SpinLockGuard _(m_write_lock);
-		if (m_tty_ctrl.draw_graphics)
-			return putchar_impl(ch);
-		return true;
+		if (!m_tty_ctrl.draw_graphics)
+			return true;
+		if (m_termios.c_oflag & OPOST)
+		{
+			if ((m_termios.c_oflag & ONLCR) && ch == '\n')
+				return putchar_impl('\r') && putchar_impl('\n');
+			if ((m_termios.c_oflag & OCRNL) && ch == '\r')
+				return putchar_impl('\n');
+		}
+		return putchar_impl(ch);
 	}
 
 	BAN::ErrorOr<size_t> TTY::read_impl(off_t, BAN::ByteSpan buffer)
@@ -353,7 +368,13 @@ namespace Kernel
 			return 0;
 		}
 
-		size_t to_copy = BAN::Math::min<size_t>(buffer.size(), m_output.bytes);
+		const size_t max_to_copy = BAN::Math::min<size_t>(buffer.size(), m_output.bytes);
+		size_t to_copy = max_to_copy;
+		if (m_termios.c_lflag & ICANON)
+			for (to_copy = 1; to_copy < max_to_copy; to_copy++)
+				if (m_output.buffer[to_copy - 1] == '\n')
+					break;
+
 		memcpy(buffer.data(), m_output.buffer.data(), to_copy);
 
 		memmove(m_output.buffer.data(), m_output.buffer.data() + to_copy, m_output.bytes - to_copy);
diff --git a/kernel/kernel/Terminal/VirtualTTY.cpp b/kernel/kernel/Terminal/VirtualTTY.cpp
index 29b3aeb3ca..8bf3a572a1 100644
--- a/kernel/kernel/Terminal/VirtualTTY.cpp
+++ b/kernel/kernel/Terminal/VirtualTTY.cpp
@@ -36,7 +36,15 @@ namespace Kernel
 	}
 
 	VirtualTTY::VirtualTTY(BAN::RefPtr<TerminalDriver> driver)
-		: TTY(0600, 0, 0)
+		: TTY({
+			.c_iflag = 0,
+			.c_oflag = 0,
+			.c_cflag = CS8,
+			.c_lflag = ECHO | ICANON,
+			.c_cc = {},
+			.c_ospeed = B38400,
+			.c_ispeed = B38400,
+		  }, 0600, 0, 0)
 		, m_name(MUST(BAN::String::formatted("tty{}", s_next_tty_number++)))
 		, m_terminal_driver(driver)
 		, m_palette(driver->palette())