From ab8d87cdda943bd905d5f0c65517f51e433e5250 Mon Sep 17 00:00:00 2001 From: ftalpo01 <f.talpo@cineca.it> Date: Thu, 6 Mar 2025 11:52:55 +0100 Subject: [PATCH] Fixes --- modplanner/cli/cmd/query.py | 4 +-- modplanner/main.py | 2 +- modplanner/service/dispatch.py | 2 +- modplanner/tty/modules.py | 10 +++++++- modplanner/tty/tty.py | 40 +++++++++++++++++++++++++++--- modplanner/tui/__init__.py | 18 ++++++++++++++ modplanner/tui/tui.py | 45 ++++++++++++++++++++++++++++++++++ setup.cfg | 6 +++++ 8 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 modplanner/tui/__init__.py create mode 100644 modplanner/tui/tui.py diff --git a/modplanner/cli/cmd/query.py b/modplanner/cli/cmd/query.py index 6cf4e97..2d5efb4 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 03ba018..ff3c2d4 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 53201fa..1d73f63 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 baf51c6..e91178e 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 d38a1fb..cfc91bf 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 0000000..6263177 --- /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 0000000..ca98b88 --- /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 2990a36..65197f0 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 = ./ -- GitLab