[minimal][tui] added tui capabilities
:Release Notes: - Why? For which reason? Just because... :Detailed Notes: - moved minimal as subcommand to argparse for dividing arguments specific for some modules only :Testing Performed: - Direct launch with tui enabled :QA Notes: - :Issues Addressed: -
This commit is contained in:
parent
c3b831d519
commit
00a8bae652
@ -1,3 +1,14 @@
|
|||||||
|
linkify-it-py==2.1.0
|
||||||
|
markdown-it-py==4.2.0
|
||||||
|
mdit-py-plugins==0.6.1
|
||||||
|
mdurl==0.1.2
|
||||||
|
platformdirs==4.10.0
|
||||||
|
Pygments==2.20.0
|
||||||
|
rich==15.0.0
|
||||||
|
textual==8.2.8
|
||||||
|
textual-autocomplete==4.0.6
|
||||||
tree-sitter==0.26.0
|
tree-sitter==0.26.0
|
||||||
tree-sitter-cpp==0.23.4
|
tree-sitter-cpp==0.23.4
|
||||||
tree-sitter-python==0.25.0
|
tree-sitter-python==0.25.0
|
||||||
|
typing_extensions==4.16.0
|
||||||
|
uc-micro-py==2.0.0
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import ts_minimal as tsmin
|
import ts_minimal as tsmin
|
||||||
|
import tui
|
||||||
from arg_conf import parser, setup_parser
|
from arg_conf import parser, setup_parser
|
||||||
from log_conf import logger, logger_setup
|
from log_conf import logger, logger_setup
|
||||||
|
|
||||||
@ -22,20 +24,38 @@ def main_arguments():
|
|||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"-l", "--log-file", help="File in which log messages will go", default="ts.log"
|
"-l", "--log-file", help="File in which log messages will go", default="ts.log"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-t",
|
||||||
|
"--tui",
|
||||||
|
help="Enable TUI for this run",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
parser.add_argument("-s", "--severity", help="Log severity level", default="DEBUG")
|
parser.add_argument("-s", "--severity", help="Log severity level", default="DEBUG")
|
||||||
tsmin.arguments()
|
tsmin.arguments()
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
return args
|
|
||||||
|
|
||||||
|
return args
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
global logger
|
global logger
|
||||||
args = main_arguments()
|
args = main_arguments()
|
||||||
logger_setup("Seminar-Test", parse_severity(args.severity))
|
handler = None
|
||||||
|
app: Optional[tui.ConsoleApp] = None
|
||||||
|
if args.tui:
|
||||||
|
app = tui.ConsoleApp()
|
||||||
|
handler = tui.RichLogHandler(app)
|
||||||
|
|
||||||
|
logger_setup("Seminar-Test", parse_severity(args.severity), handler)
|
||||||
logger.info("Hi!")
|
logger.info("Hi!")
|
||||||
|
|
||||||
tsmin.simple_file()
|
if args.command == 'minimal' and app is not None:
|
||||||
|
runner = tsmin.SingleFileParser(args)
|
||||||
|
app.add_executor(runner)
|
||||||
|
|
||||||
|
if args.tui and app is not None:
|
||||||
|
app.run()
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
src/ts-example/command_runner.py
Normal file
10
src/ts-example/command_runner.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
class ICommandRunner(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def run_command(self, command: str):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_commands(self) -> list[str]:
|
||||||
|
pass
|
||||||
@ -1,24 +1,22 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
logger = logging.getLogger("Seminar-Test")
|
logger = logging.getLogger("Seminar-Test")
|
||||||
|
|
||||||
|
|
||||||
def logger_setup(app_name: str, logging_level) -> logging.Logger:
|
def logger_setup(app_name: str, logging_level, handler: Optional[logging.Handler]) -> logging.Logger:
|
||||||
logger.name = app_name
|
logger.name = app_name
|
||||||
logger.handlers.clear()
|
logger.handlers.clear()
|
||||||
logger.setLevel(logging_level)
|
logger.setLevel(logging_level)
|
||||||
# # create file handler which logs even debug messages
|
|
||||||
# fh = logging.FileHandler("spam.log")
|
|
||||||
# fh.setLevel(logging_level)
|
|
||||||
# create console handler with a higher log level
|
|
||||||
ch = logging.StreamHandler()
|
|
||||||
ch.setLevel(logging_level)
|
|
||||||
# create formatter and add it to the handlers
|
|
||||||
formatter = logging.Formatter("[%(asctime)s] [%(name)s:%(levelname)s] %(message)s")
|
formatter = logging.Formatter("[%(asctime)s] [%(name)s:%(levelname)s] %(message)s")
|
||||||
# fh.setFormatter(formatter)
|
if handler is None:
|
||||||
ch.setFormatter(formatter)
|
ch = logging.StreamHandler()
|
||||||
# add the handlers to the logger
|
ch.setLevel(logging_level)
|
||||||
# logger.addHandler(fh)
|
ch.setFormatter(formatter)
|
||||||
logger.addHandler(ch)
|
logger.addHandler(ch)
|
||||||
|
else:
|
||||||
|
handler.setLevel(logging_level)
|
||||||
|
handler.setFormatter(formatter)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
|
||||||
return logger
|
return logger
|
||||||
|
|||||||
@ -1,35 +1,72 @@
|
|||||||
import logging
|
import tempfile
|
||||||
from sys import stdout
|
from command_runner import ICommandRunner
|
||||||
from arg_conf import parser as arg_parser
|
from arg_conf import parser as arg_parser
|
||||||
from log_conf import logger
|
from log_conf import logger
|
||||||
import tree_sitter_python as tspython
|
import tree_sitter_python as tspython
|
||||||
import tree_sitter_cpp as tscpp
|
import tree_sitter_cpp as tscpp
|
||||||
from tree_sitter import Language, Parser
|
from tree_sitter import Language, Parser, Tree
|
||||||
|
|
||||||
PY_LANGUAGE = Language(tspython.language())
|
PY_LANGUAGE = Language(tspython.language())
|
||||||
CPP_LANGUAGE = Language(tscpp.language())
|
CPP_LANGUAGE = Language(tscpp.language())
|
||||||
|
|
||||||
def arguments():
|
def arguments():
|
||||||
global arg_parser
|
global arg_parser
|
||||||
arg_parser.add_argument("--language", help="Setup parsing language", default="Python")
|
subparsers = arg_parser.add_subparsers(
|
||||||
arg_parser.add_argument("--file", "-f", help="File to parse", required=True)
|
dest="command",
|
||||||
|
required=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
module_parser = subparsers.add_parser("minimal")
|
||||||
|
|
||||||
|
module_parser.add_argument("--language", help="Setup parsing language", default="Python")
|
||||||
|
module_parser.add_argument("--file", "-f", help="File to parse", required=True)
|
||||||
logger.debug("Added Tree Sitter Minimal Example Arguments to the list")
|
logger.debug("Added Tree Sitter Minimal Example Arguments to the list")
|
||||||
|
|
||||||
|
class SingleFileParser(ICommandRunner):
|
||||||
|
def __init__ (self, args):
|
||||||
|
self.tree: Tree
|
||||||
|
self.parser: Parser
|
||||||
|
if args.language == 'Python':
|
||||||
|
self.parser = Parser(PY_LANGUAGE)
|
||||||
|
elif args.language == 'C++':
|
||||||
|
self.parser = Parser(CPP_LANGUAGE)
|
||||||
|
else:
|
||||||
|
self.parser = Parser(PY_LANGUAGE)
|
||||||
|
args.language = 'Python'
|
||||||
|
logger.info(f"Parser is installed to {args.language}")
|
||||||
|
|
||||||
|
|
||||||
|
def get_usages(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def read_file(self, filename: str):
|
||||||
|
with open(filename, 'rb') as file:
|
||||||
|
source_code = file.read()
|
||||||
|
self.tree = self.parser.parse(source_code)
|
||||||
|
|
||||||
|
def dump_tree(self):
|
||||||
|
with tempfile.TemporaryFile(mode="w+b", buffering=0) as output:
|
||||||
|
self.tree.print_dot_graph(output)
|
||||||
|
|
||||||
|
output.seek(0)
|
||||||
|
graph = output.read().decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
logger.info("Tree graph:\n%s", graph.rstrip())
|
||||||
|
|
||||||
|
def run_command(self, command: str):
|
||||||
|
global arg_parser
|
||||||
|
args = arg_parser.parse_args()
|
||||||
|
if command == "simple_file":
|
||||||
|
self.read_file(args.file)
|
||||||
|
self.dump_tree()
|
||||||
|
|
||||||
|
def get_commands(self) -> list[str]:
|
||||||
|
return ["simple_file"]
|
||||||
|
|
||||||
def simple_file():
|
def simple_file():
|
||||||
global arg_parser
|
global arg_parser
|
||||||
args = arg_parser.parse_args()
|
args = arg_parser.parse_args()
|
||||||
parser: Parser
|
|
||||||
if args.language == 'Python':
|
|
||||||
parser = Parser(PY_LANGUAGE)
|
|
||||||
elif args.language == 'C++':
|
|
||||||
parser = Parser(CPP_LANGUAGE)
|
|
||||||
else:
|
|
||||||
parser = Parser(PY_LANGUAGE)
|
|
||||||
args.language = 'Python'
|
|
||||||
|
|
||||||
logger.info(f"Parser is installed to {args.language}")
|
tree_parser = SingleFileParser(args)
|
||||||
|
tree_parser.read_file(args.file)
|
||||||
with open(args.file, 'rb') as file:
|
tree_parser.dump_tree()
|
||||||
source_code = file.read()
|
|
||||||
tree = parser.parse(source_code)
|
|
||||||
tree.print_dot_graph(stdout)
|
|
||||||
|
|||||||
181
src/ts-example/tui.py
Normal file
181
src/ts-example/tui.py
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from textual import on
|
||||||
|
from textual.app import App, ComposeResult
|
||||||
|
from command_runner import ICommandRunner
|
||||||
|
from textual.message import Message
|
||||||
|
from textual.widgets import Input, RichLog
|
||||||
|
from textual.geometry import Offset, Region, Spacing
|
||||||
|
from textual_autocomplete import AutoComplete
|
||||||
|
|
||||||
|
class AutoCompleteAbove(AutoComplete):
|
||||||
|
def _align_to_target(self) -> None:
|
||||||
|
"""Place the autocomplete list above the input cursor."""
|
||||||
|
cursor_x, cursor_y = self.target.cursor_screen_offset
|
||||||
|
|
||||||
|
dropdown = self.option_list
|
||||||
|
width, height = dropdown.outer_size
|
||||||
|
|
||||||
|
# Align horizontally like the original implementation,
|
||||||
|
# but place the bottom of the dropdown above the input.
|
||||||
|
desired_x = cursor_x - 1
|
||||||
|
desired_y = cursor_y - height
|
||||||
|
|
||||||
|
x, y, _, _ = Region(
|
||||||
|
desired_x,
|
||||||
|
desired_y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
).constrain(
|
||||||
|
"inside",
|
||||||
|
"none",
|
||||||
|
Spacing.all(0),
|
||||||
|
self.screen.scrollable_content_region,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.absolute_offset = Offset(x, y)
|
||||||
|
|
||||||
|
class LogLine(Message):
|
||||||
|
"""A line to display in the TUI output pane."""
|
||||||
|
|
||||||
|
def __init__(self, text: str) -> None:
|
||||||
|
self.text = text
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
|
||||||
|
class RichLogHandler(logging.Handler):
|
||||||
|
"""Send standard logging records to the Textual app."""
|
||||||
|
|
||||||
|
def __init__(self, app: "ConsoleApp") -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.app = app
|
||||||
|
|
||||||
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
|
try:
|
||||||
|
text = self.format(record)
|
||||||
|
|
||||||
|
# post_message is safe even when logging happens
|
||||||
|
# in another thread.
|
||||||
|
self.app.post_message(LogLine(text))
|
||||||
|
except Exception:
|
||||||
|
self.handleError(record)
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleApp(App[None]):
|
||||||
|
AUTO_FOCUS = "#command"
|
||||||
|
|
||||||
|
CSS = """
|
||||||
|
Screen {
|
||||||
|
layout: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
#output {
|
||||||
|
height: 1fr;
|
||||||
|
border: round $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
#command {
|
||||||
|
dock: bottom;
|
||||||
|
height: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
AutoCompleteAbove AutoCompleteList {
|
||||||
|
max-height: 10;
|
||||||
|
padding: 0 1;
|
||||||
|
border: round $primary;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
def compose(self) -> ComposeResult:
|
||||||
|
yield RichLog(
|
||||||
|
id="output",
|
||||||
|
wrap=True,
|
||||||
|
auto_scroll=True,
|
||||||
|
markup=False,
|
||||||
|
max_lines=2_000,
|
||||||
|
)
|
||||||
|
textInput = Input(
|
||||||
|
placeholder="Type a command and press Enter",
|
||||||
|
id="command",
|
||||||
|
)
|
||||||
|
candidates = self.get_commands()
|
||||||
|
if self.runner is not None:
|
||||||
|
candidates += self.runner.get_commands()
|
||||||
|
yield textInput
|
||||||
|
yield AutoCompleteAbove(
|
||||||
|
textInput,
|
||||||
|
candidates=candidates
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_executor(self, runner: ICommandRunner):
|
||||||
|
self.runner = runner
|
||||||
|
|
||||||
|
def on_mount(self) -> None:
|
||||||
|
self._log_handler = RichLogHandler(self)
|
||||||
|
self._log_handler.setFormatter(
|
||||||
|
logging.Formatter(
|
||||||
|
"%(asctime)s %(levelname)s %(message)s",
|
||||||
|
datefmt="%H:%M:%S",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Replace terminal-based logging handlers while the TUI is running.
|
||||||
|
self._root_logger = logging.getLogger()
|
||||||
|
self._previous_handlers = self._root_logger.handlers[:]
|
||||||
|
self._previous_level = self._root_logger.level
|
||||||
|
|
||||||
|
for handler in self._previous_handlers:
|
||||||
|
self._root_logger.removeHandler(handler)
|
||||||
|
|
||||||
|
self._root_logger.addHandler(self._log_handler)
|
||||||
|
self._root_logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
logging.info("Application started")
|
||||||
|
|
||||||
|
def on_unmount(self) -> None:
|
||||||
|
# Restore the application's previous logging configuration.
|
||||||
|
self._root_logger.removeHandler(self._log_handler)
|
||||||
|
|
||||||
|
for handler in self._previous_handlers:
|
||||||
|
self._root_logger.addHandler(handler)
|
||||||
|
|
||||||
|
self._root_logger.setLevel(self._previous_level)
|
||||||
|
|
||||||
|
@on(LogLine)
|
||||||
|
def display_log_line(self, message: LogLine) -> None:
|
||||||
|
self.query_one("#output", RichLog).write(message.text)
|
||||||
|
|
||||||
|
@on(Input.Submitted, "#command")
|
||||||
|
def handle_command(self, event: Input.Submitted) -> None:
|
||||||
|
command = event.value.strip()
|
||||||
|
event.input.clear()
|
||||||
|
|
||||||
|
if not command:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.post_message(LogLine(f"> {command}"))
|
||||||
|
|
||||||
|
if command in {"quit", "exit"}:
|
||||||
|
self.exit()
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.run_command(command)
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Command failed")
|
||||||
|
|
||||||
|
def get_commands(self) -> list[str]:
|
||||||
|
return ["help", "error", "quit", "exit"]
|
||||||
|
|
||||||
|
def run_command(self, command: str) -> None:
|
||||||
|
"""Replace this with your application logic."""
|
||||||
|
|
||||||
|
if command == "help":
|
||||||
|
logging.info("Commands: help, error, quit")
|
||||||
|
|
||||||
|
elif command == "error":
|
||||||
|
raise RuntimeError("Example failure")
|
||||||
|
|
||||||
|
else:
|
||||||
|
logging.info(f"Delegated {command} to runner")
|
||||||
|
self.runner.run_command(command)
|
||||||
Loading…
x
Reference in New Issue
Block a user