"""StatusBar -- bottom status bar showing file, cursor, diagnostics, debug state."""
import logging
from typing import TYPE_CHECKING
from simvx.core import Vec2
from simvx.core.ui.core import Control
from simvx.core.ui.theme import get_theme
if TYPE_CHECKING:
from ..config import Config
from ..state import State
log = logging.getLogger(__name__)
_FONT_SIZE = 13.0
_PAD = 8.0
[docs]
class StatusBar(Control):
"""28px status bar at the bottom of the IDE window.
Displays: file path | Ln X, Col Y | language | encoding | diagnostics | LSP | lint | debug
"""
def __init__(self, state: State, config: Config, **kwargs):
super().__init__(**kwargs)
self._state = state
self._config = config
self.size = Vec2(config.window_width, 28)
# Cached display values
self._file_path: str = ""
self._cursor_text: str = "Ln 1, Col 1"
self._language: str = ""
self._encoding: str = "UTF-8"
self._error_count: int = 0
self._warning_count: int = 0
self._debug_state: str = ""
self._status_msg: str = ""
self._status_timer: float = 0.0
self._lsp_status: str = "off" # off, starting, ready, error
self._bookmark_count: int = 0
# Connect signals
state.active_file_changed.connect(self._on_file_changed)
state.cursor_moved.connect(self._on_cursor_moved)
state.diagnostics_updated.connect(self._on_diagnostics_updated)
state.debug_state_changed.connect(self._on_debug_state_changed)
state.status_message.connect(self._on_status_message)
state.bookmark_toggled.connect(self._on_bookmark_toggled)
def _on_file_changed(self, path: str):
self._file_path = self._state.relative_path(path) if path else ""
# Detect language from file extension
if path:
from pathlib import Path as P
ext = P(path).suffix.lower()
lang_map = {".py": "Python", ".js": "JavaScript", ".ts": "TypeScript", ".json": "JSON",
".toml": "TOML", ".yaml": "YAML", ".yml": "YAML", ".md": "Markdown",
".html": "HTML", ".css": "CSS", ".glsl": "GLSL", ".rs": "Rust",
".go": "Go", ".c": "C", ".cpp": "C++", ".h": "C", ".sh": "Shell",
".txt": "Text", ".xml": "XML", ".ini": "INI", ".cfg": "Config"}
self._language = lang_map.get(ext, ext.lstrip(".").upper() if ext else "")
else:
self._language = ""
self._encoding = "UTF-8"
def _on_cursor_moved(self, line: int, col: int):
self._cursor_text = f"Ln {line}, Col {col}"
def _on_diagnostics_updated(self, path: str, diagnostics: list):
# Aggregate from all files
all_diags = self._state.all_diagnostics
self._error_count = sum(sum(1 for d in diags if d.severity == 1) for diags in all_diags.values())
self._warning_count = sum(sum(1 for d in diags if d.severity == 2) for diags in all_diags.values())
def _on_debug_state_changed(self, state_name: str, data=None):
self._debug_state = state_name
def _on_status_message(self, message: str):
self._status_msg = message
self._status_timer = 5.0
def _on_bookmark_toggled(self, path: str, line: int):
"""Update bookmark count from all files."""
total = sum(len(bm) for bm in self._state.all_bookmarks.values())
self._bookmark_count = total
@property
def lsp_status(self) -> str:
"""LSP status: 'off', 'starting', 'ready', 'error'."""
return self._lsp_status
[docs]
@lsp_status.setter
def lsp_status(self, status: str):
self._lsp_status = status
[docs]
def process(self, dt: float):
if self._status_timer > 0:
self._status_timer -= dt
if self._status_timer <= 0:
self._status_msg = ""
[docs]
def draw(self, renderer):
theme = get_theme()
x, y, w, h = self.get_global_rect()
scale = _FONT_SIZE / 14.0
# Background
renderer.draw_rect((x, y), (w, h), colour=theme.status_bar_bg, filled=True)
# Top border line
renderer.draw_line((x, y), (x + w, y), colour=theme.border)
cx = x + _PAD
# Status message (temporary, takes priority over file path)
if self._status_msg:
renderer.draw_text(
self._status_msg, (cx, y + (h - _FONT_SIZE) / 2), colour=theme.accent, scale=scale
)
return
# File path
if self._file_path:
renderer.draw_text(
self._file_path, (cx, y + (h - _FONT_SIZE) / 2), colour=theme.text, scale=scale
)
cx += renderer.text_width(self._file_path, scale) + _PAD * 2
# Separator
if self._file_path:
renderer.draw_line((cx, y + 4), (cx, y + h - 4), colour=theme.border_light)
cx += _PAD
# Right-aligned items (build from right edge)
rx = x + w - _PAD
ty = y + (h - _FONT_SIZE) / 2
# Debug state (rightmost)
if self._debug_state:
dw = renderer.text_width(self._debug_state, scale)
rx -= dw
renderer.draw_text(self._debug_state, (rx, ty), colour=theme.accent, scale=scale)
rx -= _PAD * 2
# LSP status
lsp_colours = {"off": theme.text_dim, "starting": theme.warning, "ready": theme.success, "error": theme.error}
lsp_label = f"LSP: {self._lsp_status}"
lsp_colour = lsp_colours.get(self._lsp_status, theme.text_dim)
lsp_w = renderer.text_width(lsp_label, scale)
rx -= lsp_w
renderer.draw_text(lsp_label, (rx, ty), colour=lsp_colour, scale=scale)
rx -= _PAD * 2
# Lint status
lint_label = "Lint: on" if self._config.lint_enabled else "Lint: off"
lint_colour = theme.success if self._config.lint_enabled else theme.text_dim
lint_w = renderer.text_width(lint_label, scale)
rx -= lint_w
renderer.draw_text(lint_label, (rx, ty), colour=lint_colour, scale=scale)
rx -= _PAD * 2
# Diagnostics
diag_parts = []
if self._error_count > 0:
diag_parts.append((f"E:{self._error_count}", theme.error))
if self._warning_count > 0:
diag_parts.append((f"W:{self._warning_count}", theme.warning))
for text, colour in reversed(diag_parts):
tw = renderer.text_width(text, scale)
rx -= tw
renderer.draw_text(text, (rx, ty), colour=colour, scale=scale)
rx -= _PAD
# Bookmarks
if self._bookmark_count > 0:
bm_text = f"BM: {self._bookmark_count}"
bm_w = renderer.text_width(bm_text, scale)
rx -= bm_w
renderer.draw_text(bm_text, (rx, ty), colour=theme.accent, scale=scale)
rx -= _PAD * 2
# Encoding
if self._encoding:
ew = renderer.text_width(self._encoding, scale)
rx -= ew
renderer.draw_text(self._encoding, (rx, ty), colour=theme.text, scale=scale)
rx -= _PAD * 2
# Language
if self._language:
lw = renderer.text_width(self._language, scale)
rx -= lw
renderer.draw_text(self._language, (rx, ty), colour=theme.text, scale=scale)
rx -= _PAD * 2
# Cursor position
cw = renderer.text_width(self._cursor_text, scale)
rx -= cw
renderer.draw_text(self._cursor_text, (rx, ty), colour=theme.text, scale=scale)