diff --git a/CHANGELOG.md b/CHANGELOG.md index d0a994f..8b843d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Calendar Versioning](https://calver.org). +## [24.5] - 2024-10-16 + +### Removed + +Syntax optional dependency. It's causing issues with newer Python versions. + +## [24.4] - 2024-10-16 + +### Fixed + +Crash when pressing the down key. Thank you @ALERTua! + +## [24.3] - 2024-07-05 + +### Added + +Command highlighting on Python 3.12 +Django 5.1 support + +### Fixed + +Crash on startup when used with older Textual versions +Bug that prevented commands from working + + ## [24.2] - 2024-03-29 ### Fixed diff --git a/README.md b/README.md index 7a1a6a5..06a5224 100644 --- a/README.md +++ b/README.md @@ -52,3 +52,7 @@ python manage.py tui ## License `django-tui` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license. + +## Looking for Django Admin in your termain? + +Checkout [@valberg](https://github.com/valberg)'s [django-admin-tui](https://github.com/valberg/django-admin-tui) project! diff --git a/pyproject.toml b/pyproject.toml index 4d91237..f39c228 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,14 +9,8 @@ description = 'Inspect and run Django Commands in a text-based user interface (T readme = "README.md" requires-python = ">=3.8" license = "MIT" -keywords = [ - "django", - "tui", - "textual" -] -authors = [ - { name = "Anže Pečar", email = "anze@pecar.me" }, -] +keywords = ["django", "tui", "textual"] +authors = [{ name = "Anže Pečar", email = "anze@pecar.me" }] classifiers = [ "Development Status :: 5 - Production/Stable", "Framework :: Django", @@ -25,6 +19,7 @@ classifiers = [ "Framework :: Django :: 4.1", "Framework :: Django :: 4.2", "Framework :: Django :: 5.0", + "Framework :: Django :: 5.1", "Programming Language :: Python", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", @@ -34,12 +29,7 @@ classifiers = [ "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] -dependencies = [ - "django>=3.2", - "textual[syntax]>=0.54.0 ; python_version < '3.12'", - "textual>=0.54.0 ; python_version >= '3.12'", # tree-sitter-languages doesn't support 3.12 yet (https://github.com/grantjenks/py-tree-sitter-languages/issues/30) - "trogon", -] +dependencies = ["django>=3.2", "textual>=0.64.0", "trogon"] [project.urls] Documentation = "https://github.com/anze3db/django-tui#readme" @@ -53,46 +43,24 @@ Twitter = "https://twitter.com/anze3db" path = "src/django_tui/__about__.py" [tool.hatch.envs.default] -dependencies = [ - "coverage[toml]>=6.5", - "pytest", -] +dependencies = ["coverage[toml]>=6.5", "pytest"] [tool.hatch.envs.default.scripts] test = "pytest {args:tests}" test-cov = "coverage run -m pytest {args:tests}" -cov-report = [ - "- coverage combine", - "coverage report", -] -cov = [ - "test-cov", - "cov-report", -] +cov-report = ["- coverage combine", "coverage report"] +cov = ["test-cov", "cov-report"] [[tool.hatch.envs.all.matrix]] python = ["3.8", "3.9", "3.10", "3.11", "3.12"] [tool.hatch.envs.lint] detached = true -dependencies = [ - "mypy>=1.0.0", - "ruff>=0.1.6", -] +dependencies = ["mypy>=1.0.0", "ruff>=0.1.6"] [tool.hatch.envs.lint.scripts] typing = "mypy --install-types --non-interactive {args:src/django_tui tests}" -style = [ - "ruff {args:.}", - "ruff format --check {args:.}", -] -fmt = [ - "ruff format {args:.}", - "ruff --fix {args:.}", - "style", -] -all = [ - "style", - "typing", -] +style = ["ruff {args:.}", "ruff format --check {args:.}"] +fmt = ["ruff format {args:.}", "ruff --fix {args:.}", "style"] +all = ["style", "typing"] [tool.ruff] target-version = "py38" @@ -130,9 +98,15 @@ ignore = [ # Allow boolean positional values in function calls, like `dict.get(... True)` "FBT003", # Ignore checks for possible passwords - "S105", "S106", "S107", + "S105", + "S106", + "S107", # Ignore complexity - "C901", "PLR0911", "PLR0912", "PLR0913", "PLR0915", + "C901", + "PLR0911", + "PLR0912", + "PLR0913", + "PLR0915", ] unfixable = [ # Don't touch unused imports @@ -153,17 +127,11 @@ ban-relative-imports = "all" source_pkgs = ["django_tui", "tests"] branch = true parallel = true -omit = [ - "src/django_tui/__about__.py", -] +omit = ["src/django_tui/__about__.py"] [tool.coverage.paths] django_tui = ["src/django_tui", "*/django-tui/src/django_tui"] tests = ["tests", "*/django-tui/tests"] [tool.coverage.report] -exclude_lines = [ - "no cov", - "if __name__ == .__main__.:", - "if TYPE_CHECKING:", -] +exclude_lines = ["no cov", "if __name__ == .__main__.:", "if TYPE_CHECKING:"] diff --git a/src/django_tui/__about__.py b/src/django_tui/__about__.py index 9fc5b4a..7b7cb9b 100644 --- a/src/django_tui/__about__.py +++ b/src/django_tui/__about__.py @@ -1,4 +1,4 @@ # SPDX-FileCopyrightText: 2023-present Anže Pečar # # SPDX-License-Identifier: MIT -__version__ = "24.2" +__version__ = "24.5" diff --git a/src/django_tui/management/commands/ish.py b/src/django_tui/management/commands/ish.py index f78a09b..aef47b2 100644 --- a/src/django_tui/management/commands/ish.py +++ b/src/django_tui/management/commands/ish.py @@ -215,11 +215,7 @@ def compose(self) -> ComposeResult: class DefaultImportsInfo(ModalScreen[None]): BINDINGS = [ - Binding( - "escape", - "dismiss(None)", - "Close", - ), + Binding("escape", "dismiss(None)", "Close"), ] DEFAULT_CSS = """ @@ -394,3 +390,12 @@ def action_toggle_comment(self) -> None: def action_editor_keys(self) -> None: self.app.push_screen(TextEditorBindingsInfo()) + + def action_select_mode(self, mode_id: Literal["commands", "shell"]) -> None: + if mode_id == "commands": + from django_tui.management.commands.tui import DjangoCommandBuilder + + self.app.push_screen(DjangoCommandBuilder("pyhton manage.py", "Test command name")) + + elif mode_id == "shell": + self.app.push_screen(InteractiveShellScreen("Interactive Shell")) diff --git a/src/django_tui/management/commands/tui.py b/src/django_tui/management/commands/tui.py index b44840c..44d8278 100644 --- a/src/django_tui/management/commands/tui.py +++ b/src/django_tui/management/commands/tui.py @@ -2,9 +2,7 @@ import os import shlex -import sys from pathlib import Path -from subprocess import run from typing import Any, Literal from webbrowser import open as open_url @@ -13,7 +11,7 @@ from rich.console import Console from rich.highlighter import ReprHighlighter from rich.text import Text -from textual import events, on +from textual import on from textual.app import App, AutopilotCallbackType, ComposeResult from textual.binding import Binding from textual.containers import Horizontal, Vertical, VerticalScroll @@ -27,7 +25,12 @@ Tree, ) from textual.widgets.tree import TreeNode -from trogon.introspect import ArgumentSchema, CommandSchema, MultiValueParamData, OptionSchema +from trogon.introspect import ( + ArgumentSchema, + CommandSchema, + MultiValueParamData, + OptionSchema, +) from trogon.run_command import UserCommandData from trogon.widgets.about import TextDialog from trogon.widgets.command_info import CommandInfo @@ -153,7 +156,7 @@ class AboutDialog(TextDialog): """ def __init__(self) -> None: - title = f"About django-tui" + title = "About django-tui" message = Text.from_markup( "Built with [@click=app.visit('https://github.com/textualize/textual')]Textual[/] & [@click=app.visit('https://github.com/textualize/trogon')]Trogon[/] " "by [@click=app.visit('https://pecar.me')]Anže Pečar[/].\n\n" @@ -167,16 +170,8 @@ def __init__(self) -> None: # 2 For the command screen class DjangoCommandBuilder(Screen): COMPONENT_CLASSES = {"version-string", "prompt", "command-name-syntax"} - BINDINGS = [ Binding(key="ctrl+r", action="close_and_run", description="Close & Run"), - Binding(key="ctrl+z", action="copy_command", description="Copy Command to Clipboard"), - Binding(key="ctrl+t", action="focus_command_tree", description="Focus Command Tree"), - # Binding(key="ctrl+o", action="show_command_info", description="Command Info"), - Binding(key="ctrl+s", action="focus('search')", description="Search"), - Binding(key="ctrl+j", action="select_mode('shell')", description="Shell"), - ("escape", "app.back", "Back"), - Binding(key="f1", action="about", description="About"), ] def __init__( @@ -257,46 +252,14 @@ def action_close_and_run(self) -> None: self.app.execute_on_exit = True self.app.exit() - def action_copy_command(self) -> None: - if self.command_data is None: + async def _refresh_command_form(self, node: TreeNode[CommandSchema] | None = None) -> None: + selected_command = node.data + if selected_command is None: return - if sys.platform == "win32": - copy_command = ["clip"] - elif sys.platform == "darwin": - copy_command = ["pbcopy"] - else: - copy_command = ["xclip", "-selection", "clipboard"] - - try: - command = self.click_app_name + " " + " ".join(shlex.quote(str(x)) for x in self.command_data.to_cli_args()) - run( - copy_command, - input=command, - text=True, - check=False, - ) - self.notify(f"`{command}` copied to clipboard.") - except FileNotFoundError: - self.notify(f"Could not copy to clipboard. `{copy_command[0]}` not found.", severity="error") - - def action_about(self) -> None: - self.app.push_screen(AboutDialog()) - - async def on_mount(self, event: events.Mount) -> None: - await self._refresh_command_form() - - async def _refresh_command_form(self, node: TreeNode[CommandSchema] | None = None): - if node is None: - try: - command_tree = self.query_one(CommandTree) - node = command_tree.cursor_node - except NoMatches: - return - - self.selected_command_schema = node.data - self._update_command_description(node) - self._update_execution_string_preview(self.selected_command_schema, self.command_data) + self.selected_command_schema = selected_command + self._update_command_description(selected_command) + self._update_execution_string_preview() await self._update_form_body(node) @on(Tree.NodeHighlighted) @@ -308,23 +271,23 @@ async def selected_command_changed(self, event: Tree.NodeHighlighted[CommandSche @on(CommandForm.Changed) def update_command_data(self, event: CommandForm.Changed) -> None: self.command_data = event.command_data - self._update_execution_string_preview(self.selected_command_schema, self.command_data) + self._update_execution_string_preview() - def _update_command_description(self, node: TreeNode[CommandSchema]) -> None: + def _update_command_description(self, command: TreeNode[CommandSchema]) -> None: """Update the description of the command at the bottom of the sidebar based on the currently selected node in the command tree.""" description_box = self.query_one("#home-command-description", Static) - description_text = node.data.docstring or "" + description_text = getattr(command, "docstring", "") or "" description_text = description_text.lstrip() - description_text = f"[b]{node.label if self.is_grouped_cli else self.click_app_name}[/]\n{description_text}" + description_text = f"[b]{command.name}[/]\n{description_text}" description_box.update(description_text) - def _update_execution_string_preview(self, command_schema: CommandSchema, command_data: UserCommandData) -> None: + def _update_execution_string_preview(self) -> None: """Update the preview box showing the command string to be executed""" if self.command_data is not None: command_name_syntax_style = self.get_component_rich_style("command-name-syntax") prefix = Text(f"{self.click_app_name} ", command_name_syntax_style) - new_value = command_data.to_cli_string(include_root_command=False) + new_value = self.command_data.to_cli_string(include_root_command=False) highlighted_new_value = Text.assemble(prefix, self.highlighter(new_value)) prompt_style = self.get_component_rich_style("prompt") preview_string = Text.assemble(("$ ", prompt_style), highlighted_new_value) @@ -347,6 +310,15 @@ async def _update_form_body(self, node: TreeNode[CommandSchema]) -> None: class DjangoTui(App): CSS_PATH = Path(__file__).parent / "trogon.scss" + BINDINGS = [ + Binding(key="ctrl+z", action="copy_command", description="Copy Command to Clipboard"), + Binding(key="ctrl+t", action="focus_command_tree", description="Focus Command Tree"), + # Binding(key="ctrl+o", action="show_command_info", description="Command Info"), + Binding(key="ctrl+s", action="focus('search')", description="Search"), + Binding(key="ctrl+j", action="select_mode('shell')", description="Shell"), + Binding(key="f1", action="about", description="About"), + ] + def __init__( self, *, @@ -360,12 +332,11 @@ def __init__( self.command_name = "django-tui" self.open_shell = open_shell - def on_mount(self): + def get_default_screen(self) -> DjangoCommandBuilder: if self.open_shell: - self.push_screen(InteractiveShellScreen("Interactive Shell")) + return InteractiveShellScreen("Interactive Shell") else: - self.push_screen(DjangoCommandBuilder(self.app_name, self.command_name)) - # self.push_screen(HomeScreen(self.app_name)) + return DjangoCommandBuilder(self.app_name, self.command_name) @on(Button.Pressed, "#home-exec-button") def on_button_pressed(self): @@ -426,6 +397,14 @@ def action_select_mode(self, mode_id: Literal["commands", "shell"]) -> None: elif mode_id == "shell": self.app.push_screen(InteractiveShellScreen("Interactive Shell")) + def action_copy_command(self) -> None: + command = self.app_name + " " + " ".join(shlex.quote(str(x)) for x in self.post_run_command) + self.copy_to_clipboard(command) + self.notify(f"`{command}` copied to clipboard.") + + def action_about(self) -> None: + self.app.push_screen(AboutDialog()) + class Command(BaseCommand): help = """Run and inspect Django commands in a text-based user interface (TUI)."""