diff --git a/README.md b/README.md index a1ff4a8..eb2e62e 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ python -m thymus clier ## Documentation -Please, refer to [Wiki](https://github.com/blademd/thymus/wiki) (*work in progress*) +Please, refer to [Wiki](https://github.com/blademd/thymus/wiki). ## Feedback diff --git a/olddoc.md b/olddoc.md deleted file mode 100644 index 829e27e..0000000 --- a/olddoc.md +++ /dev/null @@ -1,130 +0,0 @@ - -**!!!THIS IS THE OLD DOC, SOME INFORMATION MAY BE MISLEADING! BE CAREFUL!!!** - -## Keyboard shortcuts (only for TUI) - -- `Ctrl+D` to toggle the dark mode. Yes, it supports dark mode. All current stuff supports it why doesn't Thymus have to? -- `Ctrl+O` to open a file from any place of the tool. -- `Esc` to escape from the open file dialog. -- `Ctrl+B` to toggle the sidebar of the working screen. -- `Ctrl+S` to switch from the current screen to the default one. -- `Tab` to autocomplete a symbol or symbols inside the input box of the working screen. -- `Arrow Up/Down` to navigate over sections of the sidebar. -- `Ctrl+C` to immediately exit the application (**warning**, it does not ask you for mercy!) - -## Selecting and copying the text - -From the Textual documentation: - - Running a Textual app puts your terminal in to application mode which disables clicking and dragging to select text. - Most terminal emulators offer a modifier key which you can hold while you click and drag to restore the behavior you - may expect from the command line. The exact modifier key depends on the terminal and platform you are running on. - - - iTerm: Hold the OPTION key. - - Gnome Terminal: Hold the SHIFT key. - - Windows Terminal: Hold the SHIFT key. - - Refer to the documentation for your terminal emulator, if it is not listed above. - -# Usage - -## Context vs. global - -When you open a config file you get access to the Working view. This view allows you to navigate through the config file, filter some output, save it, etc. Thymus supports two configurable instances: context and global. The context instance describes the settings of a configuration that is opened in the Working view. Currently, there is only the JunOS context available. The global instance allows you to manipulate settings of Thymus itself. Context's settings are manipulated via the `set` command, global -- the `global set` command. - -The global settings are stored in the `thymus\settings\global.json` file. Thymus creates this directory and file at its startup if it is able to. - -## Global commands list - -- `global show themes` command lists all available highlighting themes. -- `global set theme` command allows you to choose any theme from the list. - -## Context commands list - -- `set` command allows you to configure some system-wide settings for the current context. It supports now: -- - `name` is to set the name of the context. This name is required by the `compare` modificator of the `show` command. -- - `spaces` is to set the indentation length for the `show` command. -- - `encoding` is to set the encoding for the `save` modificator. - -## Junos commands list - -The behavior of the tool mimics Junos CLI. Some commands are replaced with more handy analogs. - -- `show` command shows a configuration of the current section. It also supports an argument with the relative path (e.g., `show interfaces lo0.0` from the root or `show lo0.0` from the interfaces section). Also, `show ver`, and `show version` gives you the version of the configuration file (or not, who knows?). -- `go` command navigates you to any child section from the current one. -- `top` command without any arguments sets the current path back to the root. -- - `top show` modification allows you to see the full configuration (the configuration of the root). It also supports relative paths as the `show` does. -- - `top go` modification allows you to change the current path to any other possible section. -- `up` command without any arguments sets the current path back to the previous section. It supports a number of sections to return. If the number is bigger than the current depth `up` works as `top`. It also supports `show` instead the number, `up show` does not support any arguments or modificators at this moment. - -And some commands are for the CLI-mode: - -- `exit` (also, `stop`, `quit`, and `logout`) command to exit. -- `open` command requires two arguments (`open nos filename`): -- - the name of NOS (currently, it supports only `junos` keyword). -- - path and name of a target config file. -- `switch` command to switch among the different contexts, requires the name of the context to switch. Contexts are automatically named `vtyX`. - -The `show` command supports the modificators after the `|` symbol. Modificators can be stacked into a pipeline. Nested with the `top`, the `show` supports all the same modificators as without the nesting. There are three types of modificators: leading, passthrough, and terminating. The leading modificator can be placed **only** after the first `|` symbol of the pipeline. In the other words, this is the first modificator in the line. The passthrough modificator can be placed anywhere, and the terminating modificator can be placed **only** as the last one. - -- `filter` (*passthrough*) is for filtering the current output line by line. The modificator supports all the powers of Python's regular expressions (the search mode). You can use them with or without `"`, but for sophisticated expressions it's better to use quotes. - - > show | filter "^xe-1/\d/\d{2}$" - - > show protocols l2circuit | filter "neighbor 10.(?:\d{1,3}\.){2}15" - -- `wc_filter` (*passthrough*) is for filtering the current output taking into consideration only the names of children sections. In the other words, it allows you to filter sections by their name. The `{` in the section name **must** be omitted. The same RegExp support as for the `filter`. - - > show interfaces | wc_filter "^xe-(?\d{1}/){2}0" - -- `count` (*terminating*) is to count the lines. - - > show | count - - > show routing-options | count - -- `save` (*terminating*) is for saving the current output to a specified file. The encoding for this file is the same as the encoding of the current context but you can change it with the `set encoding` command. - - > show | save file1.conf - - > show routing-instances | save file2.conf - - > show | filter "address" | save file3.conf - -- `stubs` (*leading*) is for showing knobs of the current sections (knob is a command with the `;` symbol in the end). It does not show the nested knobs! - - > show | stubs - - > show system | stubs | count - -- `inactive` (*leading*) is for showing all inactive sections and knobs (with them parent sections) starting from this section. - - > show | inactive - - > show protocols bgp | inactive - -- `sections` (*leading*) is for showing the names of all child sections that you can visit with the `go` command from the current section. - - > show | sections - - > show interfaces | sections - -- `compare` (*leading*) is to compare the current section and its children with another context. To do so you first need to set names for both contexts with the `set name` command. The CLI-mode sets to all contexts their names by default. - - > show | compare cont01 - - > show xe-0/0/1 | compare cont01 - - -# Misc - -## Issues - -Textual is a young and powerful lib. But despite its abilities, some widgets are not good at tasks that Thymus rises. At this time, some hacks are used to draw lengthy configs (i.e., several megabytes) to the screen. Thymus shows you only the two first screens and then after every fourth scroll wheel down event, it appends one screen as a tail. You can easily notice it by putting the vertical scroll bar to the very end. In this case, there will not be a full config on the screen! That was made intentionally so you can scroll down some output without any side effects of the sub-loading. - -The sidebar shows you sections for autocompleting your current input. It works well but can be and will be enhanced later. Also, there are some performance penalties so the number of sections is limited to the current size of the screen. - -## What's next - -- Configs` analyzing. -- Other NOS`es support. \ No newline at end of file diff --git a/release_notes.md b/release_notes.md index 74b6009..464e64d 100644 --- a/release_notes.md +++ b/release_notes.md @@ -22,9 +22,9 @@ * Cisco IOS (IOS-XE), Arista EOS support. * Now you can close a working context with the Escape key (with a confirmation dialog). * Logging system with rotating files. Log files are stored in the "logs/" folder. The config file for the logging is "settings/logging.conf". -** Logs are accessible inside the Application via a modal dialog (Ctrl+L). +* * Logs are accessible inside the Application via a modal dialog (Ctrl+L). -# Changes +## Changes * Textual version 0.29.0. * Main screen was redesigned. It does not contain any active elements anymore. @@ -59,3 +59,22 @@ * If the content of two different files was the same `compare`, and `diff` calls crashed the app. * An empty input field of a working screen crashed the app after pressing the Enter key. * Minor bugs. + +# Version 0.1.3.f1-alpha + +## Changes + +* Folder structure was changed. + +## Enhancements + +* Now, JunOS has its native `match` keyword for filtering. +* After the sequence of "| " the auto-complete stops making the experience more smooth. + +## Fixes + +* IOS/EOS. The heuristics mechanism produced the double output both in the text field and the left sidebar. +* IOS/EOS. The `up` or `exit` commands did not consider the accessibility of the parent's section. Users could get useless buds. +* The `level` setting in the "logging.conf" did not work. +* IOS/EOS. The `wildcard` sub-command did not work with the path argument. +* IOS/EOS. The parser did not take into account the possible variable length of indentation in a file. diff --git a/thymus/__init__.py b/thymus/__init__.py index 73c9c23..eb3a11e 100644 --- a/thymus/__init__.py +++ b/thymus/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.1.3-alpha' +__version__ = '0.1.3.f1-alpha' CONFIG_PATH = 'thymus/settings/' CONFIG_NAME = 'thymus.json' diff --git a/thymus/app_settings.py b/thymus/app_settings.py index 2803743..9ed8705 100644 --- a/thymus/app_settings.py +++ b/thymus/app_settings.py @@ -172,7 +172,6 @@ def __init_logging(self) -> None: err_msg += f'Exception: "{err}".' errors.append(err_msg) try: - self.__logger.setLevel(logging.INFO) formatter = logging.Formatter(LOGGING_FORMAT) buf_handler = BufferingHandler(LOGGING_BUF_CAP) buf_handler.setFormatter(formatter) @@ -207,10 +206,30 @@ def __process_config(self) -> None: self.__logger.error(f'{err}') self.__is_alert = True + def __read_config(self) -> None: + if not self.__is_dir: + return + self.__logger.debug(f'Loading a configuration from: {CONFIG_PATH}{CONFIG_NAME}.') + data: dict[str, Any] = {} + with open(f'{CONFIG_PATH}{CONFIG_NAME}', encoding='utf-8') as f: + data = json.load(f) + if not data: + raise Exception(f'Reading of the config file "{CONFIG_NAME}" was failed.') + self.validate_keys(DEFAULT_GLOBALS, data, self.__validate_globals) + for platform, store in PLATFORMS.items(): + if platform_data := data.get(platform): + if not hasattr(self, f'_AppSettings__validate_{platform}_key'): + self.__logger.error(f'No validator for {platform.upper()}. Default.') + continue + validator = getattr(self, f'_AppSettings__validate_{platform}_key') + self.validate_keys(store, platform_data, validator, platform) + else: + self.__logger.warning(f'No data for {platform.upper()}. Default.') + def __save_config(self) -> None: if not self.__is_dir: return - self.__logger.debug(f'Saving a configuration into the file: {CONFIG_PATH}/{CONFIG_NAME}.') + self.__logger.debug(f'Saving a configuration into the file: {CONFIG_PATH}{CONFIG_NAME}.') data = self.globals for platform, platform_data in PLATFORMS.items(): data.update( @@ -218,7 +237,7 @@ def __save_config(self) -> None: platform: self.__platforms[platform] if self.__platforms.get(platform) else platform_data } ) - with open(f'{CONFIG_PATH}/{CONFIG_NAME}', 'w', encoding='utf-8') as f: + with open(f'{CONFIG_PATH}{CONFIG_NAME}', 'w', encoding='utf-8') as f: json.dump(data, f, indent=4) f.flush() os.fsync(f.fileno()) @@ -317,26 +336,6 @@ def __validate_eos_key(self, key: str, value: str | int) -> str | int: self.__logger.warning(f'Unknown EOS attribute: {key}. Ignore.') return value - def __read_config(self) -> None: - if not self.__is_dir: - return - self.__logger.debug(f'Loading a configuration from: {CONFIG_PATH}/{CONFIG_NAME}.') - data: dict[str, Any] = {} - with open(f'{CONFIG_PATH}/{CONFIG_NAME}', encoding='utf-8') as f: - data = json.load(f) - if not data: - raise Exception(f'Reading of the config file "{CONFIG_NAME}" was failed.') - self.validate_keys(DEFAULT_GLOBALS, data, self.__validate_globals) - for platform, store in PLATFORMS.items(): - if platform_data := data.get(platform): - if not hasattr(self, f'_AppSettings__validate_{platform}_key'): - self.__logger.error(f'No validator for {platform.upper()}. Default.') - continue - validator = getattr(self, f'_AppSettings__validate_{platform}_key') - self.validate_keys(store, platform_data, validator, platform) - else: - self.__logger.warning(f'No data for {platform.upper()}. Default.') - def process_command(self, command: str) -> SettingsResponse: if not command.startswith('global '): return SettingsResponse.error('Unknown global command.') diff --git a/thymus/contexts/context.py b/thymus/contexts/context.py index dcaa4d5..7cb1105 100644 --- a/thymus/contexts/context.py +++ b/thymus/contexts/context.py @@ -3,6 +3,7 @@ from functools import reduce from collections import deque from typing import TYPE_CHECKING +from logging import Logger, getLogger from ..responses import AlertResponse from ..lexers import CommonLexer @@ -30,6 +31,7 @@ class Context: '__content', '__encoding', '__spaces', + '__logger', ) __names_cache: list[tuple[Context, str]] = [] delimiter: str = '^' @@ -74,6 +76,10 @@ def spaces(self) -> int: def nos_type(self) -> str: return '' + @property + def logger(self) -> Logger: + return self.__logger + @spaces.setter def spaces(self, value: int) -> None: if value not in (1, 2, 4): @@ -93,16 +99,23 @@ def name(self, value: str) -> None: @encoding.setter def encoding(self, value: str) -> None: try: - 'shlop'.encode(value) + 'schlop'.encode(value) self.__encoding = value except LookupError: raise ValueError(f'"{value}" is not a correct encoding.') + @logger.setter + def logger(self, value: Logger) -> None: + if type(value) is not Logger: + raise ValueError('Incorrect type of a logger.') + self.__logger = value + def __init__(self, name: str, content: list[str], encoding='utf-8-sig') -> None: self.__name = name self.__content = content self.__encoding = encoding self.__spaces = 2 + self.__logger = getLogger() def free(self) -> None: if (type(self), self.__name) in self.__names_cache: diff --git a/thymus/contexts/eos.py b/thymus/contexts/eos.py index 8916f14..f48c6cb 100644 --- a/thymus/contexts/eos.py +++ b/thymus/contexts/eos.py @@ -2,6 +2,7 @@ from . import IOSContext + class EOSContext(IOSContext): def __init__(self, name: str, content: list[str], encoding: str = 'utf-8-sig') -> None: super().__init__(name, content, encoding) diff --git a/thymus/contexts/ios.py b/thymus/contexts/ios.py index c282951..ca24bf5 100644 --- a/thymus/contexts/ios.py +++ b/thymus/contexts/ios.py @@ -91,7 +91,8 @@ def __init__(self, name: str, content: list[str], encoding: str = 'utf-8-sig') - self.keywords['filter'].append('include') def __rebuild_tree(self) -> None: - self.command_top() + self.__tree = None + self.__cursor = None self.__tree = construct_tree( config=self.content, delimiter=self.delimiter, @@ -99,6 +100,7 @@ def __rebuild_tree(self) -> None: is_crop=self.__is_crop, is_promisc=self.__is_promisc ) + self.__cursor = self.__tree def __get_node_content(self, node: Root | Node) -> Generator[str, None, None]: return lazy_provide_config(self.content, node, self.spaces) @@ -228,7 +230,12 @@ def mod_sections(self, jump_node: Optional[Node] = None) -> Generator[str | Fabr yield '\n' yield from self.__inspect_children(node, node.path) - def mod_wildcard(self, data: Iterable[str], args: list[str]) -> Generator[str | FabricException, None, None]: + def mod_wildcard( + self, + data: Iterable[str], + args: list[str], + jump_node: Optional[Node] = None + ) -> Generator[str | FabricException, None, None]: if not data or len(args) != 1: yield FabricException('Incorrect arguments for "wildcard".') try: @@ -241,10 +248,12 @@ def mod_wildcard(self, data: Iterable[str], args: list[str]) -> Generator[str | if isinstance(head, Exception): yield head else: - if not self.__cursor.children: + node = jump_node if jump_node else self.__cursor + if not node.children: yield FabricException('No sections at this level.') yield '\n' - for path, child in self.__inspect_children(self.__cursor, self.__cursor.path, is_pair=True): + for path, child in self.__inspect_children(node, node.path, is_pair=True): + self.logger.debug(f'{path} {child.name}') if re.search(regexp, path): yield from self.__get_node_content(child) except StopIteration: @@ -324,7 +333,7 @@ def __check_leading_mod(name: str, position: int, args_count: int, args_limit: i data = self.mod_count(data, elem[1:]) break elif command in self.keywords['wildcard']: - data = self.mod_wildcard(data, elem[1:]) + data = self.mod_wildcard(data, elem[1:], jump_node) elif command in self.keywords['diff']: __check_leading_mod(command, number, len(elem[1:]), 1) data = self.mod_diff(elem[1:], jump_node) @@ -423,7 +432,8 @@ def command_up(self, args: deque[str]) -> Response: if current.name == 'root': break current = current.parent - steps -= 1 + if current.is_accessible: + steps -= 1 self.__cursor = current return AlertResponse.success() @@ -454,15 +464,12 @@ def command_set(self, args: deque[str]) -> Response: else: self.__is_crop = False self.__rebuild_tree() - analyze_heuristics(self.__tree, self.delimiter, self.__is_crop) elif command == 'promisc': if value in ('on', '1', 1): self.__is_promisc = True else: self.__is_promisc = False self.__rebuild_tree() - if self.__is_heuristics: - analyze_heuristics(self.__tree, self.delimiter, self.__is_crop) return AlertResponse.success(f'The "set {command}" was successfully modified.') else: args.appendleft(command) diff --git a/thymus/contexts/junos.py b/thymus/contexts/junos.py index 384dea6..ede9fd8 100644 --- a/thymus/contexts/junos.py +++ b/thymus/contexts/junos.py @@ -76,6 +76,8 @@ def __init__(self, name: str, content: list[str], encoding='utf-8-sig') -> None: self.__cursor: Root | Node = self.__tree self.__virtual_cursor: Root | Node = self.__tree self.spaces = 2 + if 'match' not in self.keywords['filter']: + self.keywords['filter'].append('match') if 'wc_filter' not in self.keywords['wildcard']: self.keywords['wildcard'].append('wc_filter') if 'compare' not in self.keywords['diff']: diff --git a/thymus/lexers/ios/ios.py b/thymus/lexers/ios/ios.py index 4c716de..0a631d5 100644 --- a/thymus/lexers/ios/ios.py +++ b/thymus/lexers/ios/ios.py @@ -98,6 +98,17 @@ class IOSLexer(RegexLexer): Name.Constant, stage='stager' ), + # SPECIAL: NUMERIC RD/RT (DUE TO UNKNOWN GLITCH) + wr( + r'(\s+)(rd)(\s)(\d+)(:)(\d+)(\n)', + Whitespace, + Keyword, + Whitespace, + Number, + Text, + Number, + stage='#push' + ), # REGULAR INSTRUCTION wr( r'(\s*)([^\n\s]+)', @@ -143,15 +154,6 @@ class IOSLexer(RegexLexer): Whitespace, stage='#push' ), - # NUMERIC RD/RT - wr( - r'(\s+)(\d+)(:)(\d+)(?=\s|\n)', - Whitespace, - Number, - Text, - Number, - stage='#push' - ), # MAC ADDRESS IN A PECULIAR NOTATION wr( r'(\s+)((?:[a-f0-9]{4}\.){2}[a-f0-9]{4})', diff --git a/thymus/parsers/ios/ios.py b/thymus/parsers/ios/ios.py index cc66e73..4bb71ad 100644 --- a/thymus/parsers/ios/ios.py +++ b/thymus/parsers/ios/ios.py @@ -36,7 +36,6 @@ class Root: stubs: list[str] begin: int end: int - step: int is_accessible: bool @dataclass @@ -273,7 +272,6 @@ def construct_tree( stubs=[], begin=0, end=0, - step=1, is_accessible=True ) prev_line: str = '' @@ -302,9 +300,7 @@ def construct_tree( prev_spaces = get_spaces(prev_line) spaces = get_spaces(line) if spaces > prev_spaces: - if not step: - step = spaces - prev_spaces - current.step = step + step = spaces - prev_spaces current = make_nodes(prev_line.strip(), current, delimiter) current.begin = number current.depth = spaces - prev_spaces diff --git a/thymus/tui/__init__.py b/thymus/tui/__init__.py deleted file mode 100644 index 79fc9a0..0000000 --- a/thymus/tui/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -__all__ = ( - 'OpenDialog', -) - -from .open_dialog import OpenDialog diff --git a/thymus/tui/contexts_modal.py b/thymus/tui/modals/contexts_modal.py similarity index 82% rename from thymus/tui/contexts_modal.py rename to thymus/tui/modals/contexts_modal.py index f912fe3..a7ade63 100644 --- a/thymus/tui/contexts_modal.py +++ b/thymus/tui/modals/contexts_modal.py @@ -10,7 +10,7 @@ if TYPE_CHECKING: from textual.app import ComposeResult - from ..tuier import TThymus + from ...tuier import TThymus class ContextListScreen(ModalScreen): @@ -43,6 +43,3 @@ def on_option_list_option_selected(self, event: OptionList.OptionSelected) -> No self.app.push_screen(screen) else: self.app.switch_screen(screen) # pushes selected one - self.app.logger.debug(f'Switched to screen: {screen}. Screen stack depth: {len(self.app.screen_stack)}.') - for screen_name in self.app.screen_stack: - self.app.logger.debug(f'Screen name: {str(screen_name.name)}, id: {str(screen_name.id)}.') diff --git a/thymus/tui/error_modal.py b/thymus/tui/modals/error_modal.py similarity index 100% rename from thymus/tui/error_modal.py rename to thymus/tui/modals/error_modal.py diff --git a/thymus/tui/logs_modal.py b/thymus/tui/modals/logs_modal.py similarity index 94% rename from thymus/tui/logs_modal.py rename to thymus/tui/modals/logs_modal.py index 77afe9f..93e4e50 100644 --- a/thymus/tui/logs_modal.py +++ b/thymus/tui/modals/logs_modal.py @@ -16,13 +16,13 @@ from rich.syntax import Syntax, ANSISyntaxTheme from rich.style import Style -from ..lexers import SyslogLexer +from ...lexers import SyslogLexer if TYPE_CHECKING: from textual.app import ComposeResult - from ..tuier import TThymus + from ...tuier import TThymus SYSLOG_DARK_STYLES = { Whitespace: Style(color='white'), @@ -59,7 +59,7 @@ def compose(self) -> ComposeResult: def on_show(self) -> None: control = self.query_one(TextLog) - control.write(Text('Current log (Escape to quit):', style='green italic')) + control.write(Text('Current log (Esc to quit):', style='green italic')) lexer = SyslogLexer() theme = ANSISyntaxTheme(SYSLOG_DARK_STYLES) if not self.app.dark: diff --git a/thymus/tui/quit_modal.py b/thymus/tui/modals/quit_modal.py similarity index 96% rename from thymus/tui/quit_modal.py rename to thymus/tui/modals/quit_modal.py index 2ba74cb..e5f52dc 100644 --- a/thymus/tui/quit_modal.py +++ b/thymus/tui/modals/quit_modal.py @@ -13,8 +13,8 @@ from textual.app import ComposeResult - from ..tuier import TThymus - from .working_screen import WorkingScreen + from ...tuier import TThymus + from ..working_screen.working_screen import WorkingScreen class QuitApp(ModalScreen): diff --git a/thymus/tui/open_dialog.py b/thymus/tui/open_dialog.py index 7c2ca59..5ee5611 100644 --- a/thymus/tui/open_dialog.py +++ b/thymus/tui/open_dialog.py @@ -20,8 +20,8 @@ ) from textual.widgets._directory_tree import DirectoryTree -from .working_screen import WorkingScreen -from .error_modal import ErrorScreen +from .working_screen.working_screen import WorkingScreen +from .modals.error_modal import ErrorScreen if TYPE_CHECKING: diff --git a/thymus/tui/extended_input.py b/thymus/tui/working_screen/extended_input.py similarity index 53% rename from thymus/tui/extended_input.py rename to thymus/tui/working_screen/extended_input.py index 63c710b..35bac93 100644 --- a/thymus/tui/extended_input.py +++ b/thymus/tui/working_screen/extended_input.py @@ -2,28 +2,16 @@ from typing import TYPE_CHECKING -from textual import work from textual.events import Key -from textual.widgets import ( - ListItem, - ListView, - Label, - Input, -) +from textual.widgets import Input from .path_bar import PathBar - -import sys +from .left_sidebar import LeftSidebar if TYPE_CHECKING: - if sys.version_info.major == 3 and sys.version_info.minor >= 9: - from collections.abc import Iterable - else: - from typing import Iterable - from .working_screen import WorkingScreen - from ..tuier import TThymus + from ...tuier import TThymus class ExtendedInput(Input): @@ -38,16 +26,13 @@ def action_submit(self) -> None: self.screen.draw(out) elif out := self.screen.context.on_enter(self.value): self.screen.draw(out) - self.screen.query_one('#ws-sections-list', ListView).clear() + self.screen.query_one('#ws-sections-list', LeftSidebar).clear() self.screen.query_one('#ws-path-line', PathBar).update_path() self.value = '' super().action_submit() async def on_input_changed(self, message: Input.Changed) -> None: - if message.value: - self.__update_side_bar(self.screen.context.update_virtual_cursor(message.value)) - else: - self.screen.query_one('#ws-sections-list', ListView).clear() + self.screen.query_one('#ws-sections-list', LeftSidebar).update(message.value) def _on_key(self, event: Key) -> None: if event.key == 'space': @@ -56,30 +41,14 @@ def _on_key(self, event: Key) -> None: self.value = self.value[:-1] elif event.key == 'tab': if self.value and self.cursor_position == len(self.value): - control = self.screen.query_one('#ws-sections-list', ListView) + control = self.screen.query_one('#ws-sections-list', LeftSidebar) if selected := control.highlighted_child: if selected.name != 'filler' and (match := self.screen.context.get_virtual_from(self.value)): self.value = selected.name.join(self.value.rsplit(match.strip(), 1)) self.cursor_position = len(self.value) event.stop() elif event.key == 'up': - control = self.screen.query_one('#ws-sections-list', ListView) - if control.children: - control.action_cursor_up() + self.screen.query_one('#ws-sections-list', LeftSidebar).action_cursor_up() elif event.key == 'down': - control = self.screen.query_one('#ws-sections-list', ListView) - if control.children: - control.action_cursor_down() + self.screen.query_one('#ws-sections-list', LeftSidebar).action_cursor_down() super()._on_key(event) - - @work(exclusive=True, exit_on_error=False) - async def __update_side_bar(self, data: Iterable[str]) -> None: - control = self.screen.query_one('#ws-sections-list', ListView) - limit = self.app.settings.globals['sidebar_limit'] - await control.clear() - for elem in data: - if not limit: - await control.append(ListItem(Label('...'), name='filler')) - break - await control.append(ListItem(Label(elem), name=elem)) - limit -= 1 diff --git a/thymus/tui/extended_textlog.py b/thymus/tui/working_screen/extended_textlog.py similarity index 100% rename from thymus/tui/extended_textlog.py rename to thymus/tui/working_screen/extended_textlog.py diff --git a/thymus/tui/working_screen/left_sidebar.py b/thymus/tui/working_screen/left_sidebar.py new file mode 100644 index 0000000..b1d3b68 --- /dev/null +++ b/thymus/tui/working_screen/left_sidebar.py @@ -0,0 +1,62 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from textual import work +from textual.widgets import ( + ListItem, + ListView, + Label, +) + +import sys + + +if TYPE_CHECKING: + if sys.version_info.major == 3 and sys.version_info.minor >= 9: + from collections.abc import Iterable + else: + from typing import Iterable + + from .working_screen import WorkingScreen + from ...tuier import TThymus + + +class LeftSidebar(ListView): + app: TThymus + screen: WorkingScreen + + async def add_element(self, value: str) -> None: + name = 'filler' if value == '...' else value + for child in self.children: + child: ListItem + if child.name == value: + return + await self.append(ListItem(Label(value), name=name)) + + def action_cursor_down(self) -> None: + if self.children: + super().action_cursor_down() + + def action_cursor_up(self) -> None: + if self.children: + super().action_cursor_up() + + def update(self, value: str) -> None: + if value: + if '| ' in value: + return + self.__update(self.screen.context.update_virtual_cursor(value)) + else: + self.clear() + + @work(exclusive=True, exit_on_error=False) + async def __update(self, data: Iterable[str]) -> None: + limit = int(self.app.settings.globals['sidebar_limit']) + await self.clear() + for elem in data: + if not limit: + await self.add_element('...') + break + await self.add_element(elem) + limit -= 1 diff --git a/thymus/tui/path_bar.py b/thymus/tui/working_screen/path_bar.py similarity index 96% rename from thymus/tui/path_bar.py rename to thymus/tui/working_screen/path_bar.py index 925591b..b961035 100644 --- a/thymus/tui/path_bar.py +++ b/thymus/tui/working_screen/path_bar.py @@ -8,7 +8,7 @@ if TYPE_CHECKING: from textual.geometry import Size - from ..tuier import TThymus + from ...tuier import TThymus from .working_screen import WorkingScreen diff --git a/thymus/tui/status_bar.py b/thymus/tui/working_screen/status_bar.py similarity index 100% rename from thymus/tui/status_bar.py rename to thymus/tui/working_screen/status_bar.py diff --git a/thymus/tui/working_screen.py b/thymus/tui/working_screen/working_screen.py similarity index 94% rename from thymus/tui/working_screen.py rename to thymus/tui/working_screen/working_screen.py index 8ab3cd9..465bb96 100644 --- a/thymus/tui/working_screen.py +++ b/thymus/tui/working_screen/working_screen.py @@ -12,15 +12,13 @@ Vertical, ) from textual.widgets import ( - ListView, Input, TextLog, - Static, ) from rich.text import Text from rich.syntax import Syntax -from ..contexts import ( +from ...contexts import ( JunOSContext, IOSContext, EOSContext, @@ -29,7 +27,8 @@ from .extended_input import ExtendedInput from .status_bar import StatusBar from .path_bar import PathBar -from .quit_modal import QuitScreen +from .left_sidebar import LeftSidebar +from ..modals.quit_modal import QuitScreen if TYPE_CHECKING: @@ -37,9 +36,9 @@ from textual.app import ComposeResult - from ..tuier import TThymus - from ..contexts import Context - from ..responses import Response + from ...tuier import TThymus + from ...contexts import Context + from ...responses import Response PLATFORMS = { @@ -79,6 +78,7 @@ def __init__(self, filename: str, nos_type: str, encoding: str, *args, **kwags) self.app.logger.error(m) raise Exception(m, 'logged') self.context: Context = PLATFORMS[nos_type]('', content, encoding) + self.context.logger = self.app.logger if hasattr(self.app.settings, nos_type): settings: dict[str, str | int] = getattr(self.app.settings, nos_type) for k, v in settings.items(): @@ -101,7 +101,7 @@ def __init__(self, filename: str, nos_type: str, encoding: str, *args, **kwags) def compose(self) -> ComposeResult: with Horizontal(id='ws-right-field'): with Container(id='ws-left-sidebar'): - yield ListView(id='ws-sections-list') + yield LeftSidebar(id='ws-sections-list') with Vertical(): with Container(id='ws-main-out-container'): yield ExtendedTextLog(id='ws-main-out') diff --git a/thymus/tuier.py b/thymus/tuier.py index f75042b..cbdfd86 100644 --- a/thymus/tuier.py +++ b/thymus/tuier.py @@ -17,9 +17,9 @@ ) from .app_settings import AppSettings from .tui.open_dialog import OpenDialog -from .tui.quit_modal import QuitApp -from .tui.contexts_modal import ContextListScreen -from .tui.logs_modal import LogsScreen +from .tui.modals.quit_modal import QuitApp +from .tui.modals.contexts_modal import ContextListScreen +from .tui.modals.logs_modal import LogsScreen if TYPE_CHECKING: