Skip to content

Commit

Permalink
Improve rendering libary (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
r0x0d authored Dec 12, 2024
1 parent ed859da commit 605b147
Show file tree
Hide file tree
Showing 27 changed files with 1,084 additions and 227 deletions.
4 changes: 2 additions & 2 deletions command_line_assistant/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
LoggingSchema,
OutputSchema,
)
from command_line_assistant.utils.environment import get_xdg_path
from command_line_assistant.utils.environment import get_xdg_config_path

# tomllib is available in the stdlib after Python3.11. Before that, we import
# from tomli.
Expand Down Expand Up @@ -54,7 +54,7 @@ def load_config_file() -> Config:
"""Read configuration file."""

config_dict = {}
config_file_path = Path(get_xdg_path(), *CONFIG_FILE_DEFINITION)
config_file_path = Path(get_xdg_config_path(), *CONFIG_FILE_DEFINITION)

try:
print(f"Loading configuration file from {config_file_path}")
Expand Down
67 changes: 67 additions & 0 deletions command_line_assistant/rendering/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from abc import ABC, abstractmethod
from typing import TextIO


class RenderDecorator(ABC):
"""Abstract base class for render decorators"""

@abstractmethod
def decorate(self, text: str) -> str:
pass


class OutputStreamWritter(ABC):
"""Abstract base class for output stream decorators"""

def __init__(self, stream: TextIO, end: str = "\n") -> None:
"""
Initialize the output stream decorator.
Args:
stream: The output stream to use
end: The string to append after the text (defaults to newline)
"""
self._stream = stream
self._end = end

@abstractmethod
def write(self, text: str) -> None:
"""Write the text to the output stream"""
pass

@abstractmethod
def flush(self) -> None:
"""Flush the output stream"""
pass

def execute(self, text: str) -> None:
"""
Write the text to the output stream and return the original text for chaining.
"""
if text:
self.write(text)
self.flush()


class BaseRenderer(ABC):
"""Base class for renderers providing common functionality."""

def __init__(self, stream: OutputStreamWritter) -> None:
self._stream = stream
self._decorators: dict[type, RenderDecorator] = {}

def update(self, decorator: RenderDecorator) -> None:
"""Update or add a decorator of the same type."""
self._decorators[type(decorator)] = decorator

def _apply_decorators(self, text: str) -> str:
"""Apply all decorators to the text."""
decorated_text = text
for decorator in self._decorators.values():
decorated_text = decorator.decorate(decorated_text)
return decorated_text

@abstractmethod
def render(self, text: str) -> None:
"""Render the text with all decorators applied."""
pass
9 changes: 0 additions & 9 deletions command_line_assistant/rendering/decorators/base.py

This file was deleted.

2 changes: 1 addition & 1 deletion command_line_assistant/rendering/decorators/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from colorama import Back, Fore, Style

from command_line_assistant.rendering.decorators.base import RenderDecorator
from command_line_assistant.rendering.base import RenderDecorator


class ColorDecorator(RenderDecorator):
Expand Down
2 changes: 1 addition & 1 deletion command_line_assistant/rendering/decorators/style.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from colorama import Style

from command_line_assistant.rendering.decorators.base import RenderDecorator
from command_line_assistant.rendering.base import RenderDecorator


class StyleDecorator(RenderDecorator):
Expand Down
44 changes: 43 additions & 1 deletion command_line_assistant/rendering/decorators/text.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import shutil
import textwrap
from pathlib import Path
from typing import Optional, Union

from command_line_assistant.rendering.decorators.base import RenderDecorator
from command_line_assistant.rendering.base import RenderDecorator
from command_line_assistant.utils.environment import get_xdg_state_path


class EmojiDecorator(RenderDecorator):
Expand Down Expand Up @@ -40,3 +42,43 @@ def decorate(self, text: str) -> str:
initial_indent=self._indent,
subsequent_indent=self._indent,
)


class WriteOnceDecorator(RenderDecorator):
"""Decorator that ensures content is written only once by checking a state file.
The state file is created under $XDG_STATE_HOME/command-line-assistant/legal/
"""

def __init__(self, state_filename: str = "written") -> None:
"""Initialize the write once decorator.
Args:
state_filename: Name of the state file to create/check
"""
self._state_dir = Path(get_xdg_state_path(), "command-line-assistant")
self._state_file = self._state_dir / state_filename

def _should_write(self) -> bool:
"""Check if content should be written by verifying state file existence."""
if self._state_file.exists():
return False

if not self._state_dir.exists():
# Create directory if it doesn't exist
self._state_dir.mkdir(parents=True)

# Write state file
self._state_file.write_text("1")
return True

def decorate(self, text: str) -> str:
"""Write the text only if it hasn't been written before.
Args:
text: The text to potentially write
Returns:
The text if it should be written, None otherwise
"""
return text if self._should_write else ""
22 changes: 0 additions & 22 deletions command_line_assistant/rendering/render.py

This file was deleted.

Empty file.
72 changes: 72 additions & 0 deletions command_line_assistant/rendering/renders/spinner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import itertools
import threading
import time
from dataclasses import dataclass
from typing import Iterator, Optional

from command_line_assistant.rendering.base import BaseRenderer, OutputStreamWritter
from command_line_assistant.rendering.stream import StdoutStream


@dataclass
class Frames:
default: Iterator[str] = itertools.cycle(["⠋", "⠙", "⠸", "⠴", "⠦", "⠇"])
dash: Iterator[str] = itertools.cycle(["-", "\\", "|", "/"])
circular: Iterator[str] = itertools.cycle(["◐", "◓", "◑", "◒"])
dots: Iterator[str] = itertools.cycle([". ", ".. ", "...", " ..", " .", " "])
arrows: Iterator[str] = itertools.cycle(["←", "↖", "↑", "↗", "→", "↘", "↓", "↙"])
moving: Iterator[str] = itertools.cycle(
["[ ]", "[= ]", "[== ]", "[===]", "[ ==]", "[ =]", "[ ]"]
)


class SpinnerRenderer(BaseRenderer):
def __init__(
self,
message: str,
stream: Optional[OutputStreamWritter] = None,
frames: Iterator[str] = Frames.default,
delay: float = 0.1,
clear_message: bool = False,
) -> None:
super().__init__(stream or StdoutStream())
self._message = message
self._frames = frames
self._delay = delay
self._clear_message = clear_message
self._done = threading.Event()
self._spinner_thread: Optional[threading.Thread] = None

def render(self, text: str) -> None:
"""Render text with all decorators applied."""
decorated_text = self._apply_decorators(text)
self._stream.execute(decorated_text)

def _animation(self) -> None:
while not self._done.is_set():
frame = next(self._frames)
message = self._apply_decorators(f"{frame} {self._message}")
self._stream.execute(f"\r{message}")
time.sleep(self._delay)

def start(self) -> None:
"""Start the spinner animation"""
self._done.clear()
self._spinner_thread = threading.Thread(target=self._animation)
self._spinner_thread.start()

def stop(self) -> None:
"""Stop the spinner animation"""
if self._spinner_thread:
self._done.set()
self._spinner_thread.join()
self._stream.execute("\n")
if self._clear_message:
self._stream.execute(f"\r{' ' * (len(self._message) + 2)}\r")

def __enter__(self) -> "SpinnerRenderer":
self.start()
return self

def __exit__(self, exc_type, exc_val, exc_tb) -> None:
self.stop()
16 changes: 16 additions & 0 deletions command_line_assistant/rendering/renders/text.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import shutil
from typing import Optional

from command_line_assistant.rendering.base import BaseRenderer, OutputStreamWritter
from command_line_assistant.rendering.stream import StdoutStream


class TextRenderer(BaseRenderer):
def __init__(self, stream: Optional[OutputStreamWritter] = None) -> None:
super().__init__(stream or StdoutStream())
self.terminal_width = shutil.get_terminal_size().columns

def render(self, text: str) -> None:
"""Render text with all decorators applied."""
decorated_text = self._apply_decorators(text)
self._stream.execute(decorated_text)
49 changes: 0 additions & 49 deletions command_line_assistant/rendering/spinner.py

This file was deleted.

47 changes: 47 additions & 0 deletions command_line_assistant/rendering/stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import sys

from command_line_assistant.rendering.base import (
OutputStreamWritter,
)


class StderrStream(OutputStreamWritter):
"""Decorator for outputting text to stderr"""

def __init__(self, end: str = "\n") -> None:
"""
Initialize the stderr decorator.
Args:
end: The string to append after the text (defaults to newline)
"""
super().__init__(stream=sys.stderr, end=end)

def write(self, text: str) -> None:
"""Write the text to stderr"""
self._stream.write(text + self._end)

def flush(self) -> None:
"""Flush stderr"""
self._stream.flush()


class StdoutStream(OutputStreamWritter):
"""Decorator for outputting text to stdout"""

def __init__(self, end: str = "\n") -> None:
"""
Initialize the stdout decorator.
Args:
end: The string to append after the text (defaults to newline)
"""
super().__init__(stream=sys.stdout, end=end)

def write(self, text: str) -> None:
"""Write the text to stdout"""
self._stream.write(text + self._end)

def flush(self) -> None:
"""Flush stdout"""
self._stream.flush()
2 changes: 1 addition & 1 deletion command_line_assistant/utils/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
class BaseCLICommand(ABC):
@abstractmethod
def run(self):
raise NotImplementedError("Not implemented in base class.")
pass


def add_default_command(argv):
Expand Down
Loading

0 comments on commit 605b147

Please sign in to comment.