diff --git a/src/background/documentation_server.py b/src/background/documentation_server.py index f82f4ff..2f7b941 100644 --- a/src/background/documentation_server.py +++ b/src/background/documentation_server.py @@ -1,23 +1,28 @@ from PySide6.QtCore import QThread, Slot -from src.utils.documentation import website, QuietHandler -from wsgiref.simple_server import make_server + +from src.utils.documentation import start_documentation_server class DocumentationThread(QThread): def __init__(self): super().__init__() - self._server = None # store server so we can shut it down + self._process = None # store subprocess so we can shut it down def run(self): - # launch_documentation() - self._server = make_server( - "localhost", 8000, website(), handler_class=QuietHandler - ) - while not self.isInterruptionRequested(): - self._server.handle_request() + # Start the zensical documentation server + self._process = start_documentation_server() + + # Keep thread alive until interruption is requested + if self._process: + while not self.isInterruptionRequested(): + self.msleep(100) # Check every 100ms @Slot() # slot you can connect to aboutToQuit def stop(self): self.requestInterruption() # ask the loop above to exit - if self._server: - self._server.shutdown() # unblock handle_request() + if self._process: + self._process.terminate() # terminate the subprocess + try: + self._process.wait(timeout=5) # wait up to 5 seconds + except: + self._process.kill() # force kill if it doesn't stop diff --git a/src/core/semester.py b/src/core/semester.py index baf6f5a..1d149a4 100644 --- a/src/core/semester.py +++ b/src/core/semester.py @@ -29,9 +29,9 @@ class Semester: # Class‑level defaults – will be *copied* to each instance and then # potentially overwritten in ``__init__``. # ------------------------------------------------------------------ - _year: int | None = int(str(datetime.datetime.now().year)[2:]) # 24 → 24 + _year: int | None = None # Will be set in __post_init__ _semester: str | None = None # "WiSe" or "SoSe" – set later - _month: int | None = datetime.datetime.now().month + _month: int | None = None # Will be set in __post_init__ value: str | None = None # Human‑readable label, e.g. "WiSe 23/24" # ------------------------------------------------------------------ @@ -54,11 +54,23 @@ class Semester: self.__post_init__() - def __post_init__(self) -> None: # noqa: D401 – keep original name - if self._year is None: - self._year = int(str(datetime.datetime.now().year)[2:]) + def __post_init__(self) -> None: + now = datetime.datetime.now() + if self._month is None: - self._month = datetime.datetime.now().month + self._month = now.month + + if self._year is None: + # Extract last 2 digits of current year + current_year = int(str(now.year)[2:]) + + # For winter semester in Jan-Mar, we need to use the previous year + # because WiSe started in October of the previous calendar year + if self._month <= 3: + self._year = (current_year - 1) % 100 + else: + self._year = current_year + if self._semester is None: self._generate_semester_from_month() self._compute_value() @@ -66,7 +78,7 @@ class Semester: # ------------------------------------------------------------------ # Dunder helpers # ------------------------------------------------------------------ - def __str__(self) -> str: # noqa: D401 – keep original name + def __str__(self) -> str: return self.value or "" def __repr__(self) -> str: # Helpful for debugging lists @@ -84,6 +96,7 @@ class Semester: year = self._year if self._semester == "WiSe": next_year = (year + 1) % 100 # wrap 99 → 0 + self.value = f"WiSe {year}/{next_year}" else: # SoSe self.value = f"SoSe {year}" @@ -91,7 +104,7 @@ class Semester: # ------------------------------------------------------------------ # Public API # ------------------------------------------------------------------ - def offset(self, value: int) -> "Semester": + def offset(self, value: int) -> Semester: """Return a new :class:`Semester` *value* steps away. The algorithm maps every semester to a monotonically increasing @@ -116,7 +129,7 @@ class Semester: # ------------------------------------------------------------------ # Comparison helpers # ------------------------------------------------------------------ - def isPastSemester(self, current: "Semester") -> bool: + def is_past_semester(self, current: Semester) -> bool: log.debug(f"Comparing {self} < {current}") if self.year < current.year: return True @@ -126,7 +139,7 @@ class Semester: ) # WiSe before next SoSe return False - def isFutureSemester(self, current: "Semester") -> bool: + def is_future_semester(self, current: Semester) -> bool: if self.year > current.year: return True if self.year == current.year: @@ -135,18 +148,18 @@ class Semester: ) # SoSe after WiSe of same year return False - def isMatch(self, other: "Semester") -> bool: + def is_match(self, other: Semester) -> bool: return self.year == other.year and self.semester == other.semester # ------------------------------------------------------------------ # Convenience properties # ------------------------------------------------------------------ @property - def next(self) -> "Semester": + def next(self) -> Semester: return self.offset(1) @property - def previous(self) -> "Semester": + def previous(self) -> Semester: return self.offset(-1) @property @@ -161,11 +174,11 @@ class Semester: # Static helpers # ------------------------------------------------------------------ @staticmethod - def generate_missing(start: "Semester", end: "Semester") -> list[str]: + def generate_missing(start: Semester, end: Semester) -> list[str]: """Return all consecutive semesters from *start* to *end* (inclusive).""" if not isinstance(start, Semester) or not isinstance(end, Semester): raise TypeError("start and end must be Semester instances") - if start.isFutureSemester(end) and not start.isMatch(end): + if start.is_future_semester(end) and not start.isMatch(end): raise ValueError("'start' must not be after 'end'") chain: list[Semester] = [start.value] @@ -181,7 +194,7 @@ class Semester: # Parsing helper # ------------------------------------------------------------------ @classmethod - def from_string(cls, s: str) -> "Semester": + def from_string(cls, s: str) -> Semester: """Parse a human‑readable semester label and return a :class:`Semester`. Accepted formats (case‑insensitive):: @@ -199,7 +212,7 @@ class Semester: m = re.fullmatch(pattern, s, flags=re.IGNORECASE) if not m: raise ValueError( - "invalid semester string format – expected 'SoSe YY' or 'WiSe YY/YY' (spacing flexible)" + "invalid semester string format – expected 'SoSe YY' or 'WiSe YY/YY' (spacing flexible)", ) term_raw, y1_str, y2_str = m.groups() @@ -209,7 +222,7 @@ class Semester: if term == "SoSe": if y2_str is not None: raise ValueError( - "SoSe string should not contain '/' followed by a second year" + "SoSe string should not contain '/' followed by a second year", ) return cls(year, "SoSe") diff --git a/src/ui/dialogs/parsed_titles.py b/src/ui/dialogs/parsed_titles.py index 3a5c857..083ee84 100644 --- a/src/ui/dialogs/parsed_titles.py +++ b/src/ui/dialogs/parsed_titles.py @@ -1,4 +1,3 @@ - from src.shared.logging import log from PySide6 import QtWidgets diff --git a/src/ui/dialogs/settings.py b/src/ui/dialogs/settings.py index 594ebd9..39edbb6 100644 --- a/src/ui/dialogs/settings.py +++ b/src/ui/dialogs/settings.py @@ -255,7 +255,6 @@ class Settings(QtWidgets.QDialog, _settings): def launch_settings(): - app = QtWidgets.QApplication(sys.argv) window = Settings() window.show() diff --git a/src/utils/documentation.py b/src/utils/documentation.py index a0230e7..477d78c 100644 --- a/src/utils/documentation.py +++ b/src/utils/documentation.py @@ -1,8 +1,7 @@ import logging import os -from wsgiref.simple_server import WSGIRequestHandler - -from flask import Flask, send_from_directory +import subprocess +import sys from src import LOG_DIR @@ -20,29 +19,34 @@ logger = logging.getLogger(__name__) # inherits the same file handler docport = 8000 -class QuietHandler(WSGIRequestHandler): - # suppress “GET /…” access log - def log_request(self, code="-", size="-"): - logger.info("Request: {} {}".format(self.requestline, code)) - pass - # suppress all other messages (errors, etc.) - def log_message(self, fmt, *args): - logger.error("Error: {}, Args: {}".format(fmt, args)) - pass - - -def website() -> object: - app = Flask(__name__, static_folder=os.path.join(os.getcwd(), "docs", "public")) - - # Serve the main index.html at root - @app.route("/") - def index(): - return send_from_directory(app.static_folder, "index.html") - - # Serve all other static files - @app.route("/") - def serve_static(path): - return send_from_directory(app.static_folder, path) - - return app +def start_documentation_server(): + """ + Start the Zensical documentation server as a subprocess. + + Returns: + subprocess.Popen: The subprocess object, or None if startup failed. + """ + try: + # Prepare subprocess arguments + creationflags = 0 + if sys.platform == "win32": + # Hide console window on Windows + creationflags = subprocess.CREATE_NO_WINDOW + + # Start subprocess with all output suppressed + process = subprocess.Popen( + ["uv", "run", "zensical", "serve"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + stdin=subprocess.DEVNULL, + creationflags=creationflags, + cwd=os.getcwd(), + ) + + logger.info(f"Documentation server started with PID {process.pid}") + return process + + except Exception as e: + logger.error(f"Failed to start documentation server: {e}") + return None