diff --git a/modplanner/cli/cmd/query.py b/modplanner/cli/cmd/query.py
index 6cf4e97fdf79de1a93e2d41af39a18c7fc497d9b..2d5efb4eadf32943fdfed05976fa145409729851 100644
--- a/modplanner/cli/cmd/query.py
+++ b/modplanner/cli/cmd/query.py
@@ -83,7 +83,7 @@ class QueryDefaults:
             modules_tty.print_flat(default_modules)
             return
 
-        tty.error(f"No module matches default for {self._mapping}")
+        tty.err(f"No module matches default for {self._mapping}")
 
 
 class QueryStructure:
@@ -138,7 +138,7 @@ class QueryPackage:
             )
             return
 
-        tty.error(f"No modules for package: {self._package}")
+        tty.err(f"No modules for package: {self._package}")
 
 
 @modplanner_command("query")
diff --git a/modplanner/main.py b/modplanner/main.py
index 03ba018d3a0b1e91d936951fc29559c733c32b50..ff3c2d44fa3feb003971563732df74571d4b9668 100644
--- a/modplanner/main.py
+++ b/modplanner/main.py
@@ -110,9 +110,9 @@ def main():
         command.parse_args(known_parsed)
 
         command.run(settings=settings)
+        tty.print("", space=False)
 
     except KeyboardInterrupt:
-        tty.print("")
         tty.fatal("Aborted by user; exiting")
 
     except Exception as e:
diff --git a/modplanner/service/dispatch.py b/modplanner/service/dispatch.py
index 53201fa6dd36fd23f3c1762451ca8da3bb54bf92..1d73f635838c8ea0eb8914d3ff78ec47aa24d68e 100644
--- a/modplanner/service/dispatch.py
+++ b/modplanner/service/dispatch.py
@@ -219,7 +219,7 @@ class DictStrategy:
         packages = Packages.from_modules(mods)
         for name, modlist in packages.items():
             if self._callback:
-                modlist = [self._callback(m) for m in modlist]
+                modlist = [self._callback(m, active_mappings) for m in modlist]
             self.add_child(parent=parent, child={name: modlist})
 
 
diff --git a/modplanner/tty/modules.py b/modplanner/tty/modules.py
index baf51c68bcf4416729f6f60e9b9156143b95b1be..e91178ee6a1e8da2096a010b1ddd5b48f00b63fd 100644
--- a/modplanner/tty/modules.py
+++ b/modplanner/tty/modules.py
@@ -22,6 +22,8 @@ from __future__ import annotations
 
 from typing import TYPE_CHECKING
 
+from rich.text import Text
+
 from modplanner.module import Packages
 from modplanner.tty import tree, tty
 from modplanner.service.dispatch import get_mappings_dict
@@ -37,10 +39,16 @@ if TYPE_CHECKING:
 def print_mappings(
     module_context: ModuleContext, ordered_mappings: OrderedDict[str, set[str]]
 ):
+
+    def _callback(module, active_mappings):
+        if module_context.hide_ctx.is_hidden(module, context=active_mappings):
+            return Text(module.module_name, style="grey53")
+        return module.module_name
+
     tree_dict = get_mappings_dict(
         module_context=module_context,
         ordered_mappings=ordered_mappings,
-        callback=lambda module: module.module_name,
+        callback=_callback,
     )
     printable = tree.pretty_print_tree(tree_dict)
     tty.print_renderable(printable, indentation=4)
diff --git a/modplanner/tty/tty.py b/modplanner/tty/tty.py
index d38a1fb8945914456dec14e934d27f0045fe98bf..cfc91bfc1b56626155a91e93f3fce12954257a72 100644
--- a/modplanner/tty/tty.py
+++ b/modplanner/tty/tty.py
@@ -88,6 +88,7 @@ class RichTty:
         self._style = style or TextStyle()
         self._prompt = Prompt()
         self._logger = logger or logging.getLogger(__name__)
+        self._lastchar = None
 
     def debug(self, msg, *args, **kwargs) -> None:
         return self._logger.debug(msg, *args, **kwargs)
@@ -119,6 +120,7 @@ class RichTty:
         italic: bool = False,
         underline: bool = False,
         strikethrough: bool = False,
+        space: bool = True,
     ) -> None:
         if any(
             (
@@ -144,17 +146,25 @@ class RichTty:
             sty = self._style
         sty_msg = stylize(msg, sty)
         padded = self.padding(sty_msg, sty)
+        if space:
+            self._ensure_linespaces()
+        self._update_lastchar(sty_msg)
         self._console.print(padded)
 
-    def print_renderable(self, rd: RenderableType, indentation: int = 0) -> None:
+    def print_renderable(
+        self, rd: RenderableType, *, space: bool = True, indentation: int = 0
+    ) -> None:
         msg = self.padding(rd, TextStyle(indentation=indentation))
+        if space:
+            self._ensure_linespaces()
+        self._update_lastchar(msg)
         self._console.print(msg)
 
-    def error(self, msg: str) -> None:
+    def err(self, msg: str) -> None:
         self.print(f"✖  {msg}", color="red1", bold=True)
 
     def fatal(self, msg: str) -> None:
-        self.error(msg)
+        self.err(msg)
         exit(1)
 
     def die(self, msg: str | Exception) -> None:
@@ -170,20 +180,42 @@ class RichTty:
     def success(self, msg: str) -> None:
         self.print(f"✔  {msg}", color="green", bold=True)
 
-    def prompt(self, msg: str, choices: list[str] | None = None, **kwargs) -> str:
+    def prompt(
+        self,
+        msg: str,
+        choices: list[str] | None = None,
+        *,
+        space: bool = True,
+        **kwargs,
+    ) -> str:
         if kwargs:
             sty = TextStyle(**kwargs)
         else:
             sty = self._style
         sty_msg = stylize(msg, sty)
+        if space:
+            self._ensure_linespaces()
+        self._update_lastchar(sty_msg)
         return self._prompt.ask(sty_msg, console=self._console, choices=choices)
 
+    def _update_lastchar(self, msg) -> None:
+        try:
+            self._lastchar = str(msg)[-1]
+        except IndexError:
+            self._lastchar = ""
+
+    def _ensure_linespaces(self) -> None:
+        if not self._lastchar in {"\n", "", " "}:
+            for _ in range(self._linespaces):
+                self._console.print("")
+
 
 _default_tty = RichTty()
 debug = _default_tty.debug
 info = _default_tty.info
 warning = _default_tty.warning
 error = _default_tty.error
+err = _default_tty.err
 critical = _default_tty.critical
 print = _default_tty.print
 print_renderable = _default_tty.print_renderable
diff --git a/modplanner/tui/__init__.py b/modplanner/tui/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..6263177cf82763f5c3c871d0a27ae5933fed398c
--- /dev/null
+++ b/modplanner/tui/__init__.py
@@ -0,0 +1,18 @@
+#
+# Author: Francesco Talpo
+#
+# This file is part of Modplanner.
+#
+# Modplanner is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published
+# by the Free Software Foundation, either version 3 of the License,
+# or (at your option) any later version.
+#
+# Modplanner is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+# See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Modplanner. If not, see <https://www.gnu.org/licenses/>.
+#
diff --git a/modplanner/tui/tui.py b/modplanner/tui/tui.py
new file mode 100644
index 0000000000000000000000000000000000000000..ca98b884287fe504b57550cb141869adee71bd01
--- /dev/null
+++ b/modplanner/tui/tui.py
@@ -0,0 +1,45 @@
+#
+# Author: Francesco Talpo
+#
+# This file is part of Modplanner.
+#
+# Modplanner is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published
+# by the Free Software Foundation, either version 3 of the License,
+# or (at your option) any later version.
+#
+# Modplanner is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+# See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Modplanner. If not, see <https://www.gnu.org/licenses/>.
+#
+
+
+from textual.app import App, ComposeResult
+from textual.widgets import Footer, Header
+
+
+class ModplannerTUI(App):
+    """
+    Modplanner TUI interface.
+    """
+
+    BINDINGS = [("d", "toggle_dark", "Toggle dark mode")]
+
+    def compose(self) -> ComposeResult:
+        """Create child widgets for the app."""
+        yield Header()
+        yield Footer()
+
+    def action_toggle_dark(self) -> None:
+        """An action to toggle dark mode."""
+        self.theme = (
+            "textual-dark" if self.theme == "textual-light" else "textual-light"
+        )
+
+
+def run_tui():
+    pass
diff --git a/setup.cfg b/setup.cfg
index 2990a3611d45c09b64b43512679e35718cfdcd5b..65197f0b8c28b6514a5f952d3e8f850c852a8c18 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -25,6 +25,12 @@ install_requires =
 dev =
     pytest
     black
+tui = 
+    textual
+full = 
+    pytest
+    black
+    textual
 
 [options.packages.find]
 where = ./