"""StatusBar -- bottom status bar showing file, cursor, diagnostics, debug state."""
from __future__ import annotations
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 IDEConfig
from ..state import IDEState
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: IDEState, config: IDEConfig, **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.get_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.get_all_bookmarks().values())
self._bookmark_count = total
[docs]
def set_lsp_status(self, status: str):
"""Update LSP status: 'off', 'starting', 'ready', 'error'."""
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_filled_rect(x, y, w, h, theme.status_bar_bg)
# Top border line
renderer.draw_line_coloured(x, y, x + w, y, theme.border)
cx = x + _PAD
# Status message (temporary, takes priority over file path)
if self._status_msg:
renderer.draw_text_coloured(self._status_msg, cx, y + (h - _FONT_SIZE) / 2, scale, theme.accent)
return
# File path
if self._file_path:
renderer.draw_text_coloured(self._file_path, cx, y + (h - _FONT_SIZE) / 2, scale, theme.text)
cx += renderer.text_width(self._file_path, scale) + _PAD * 2
# Separator
if self._file_path:
renderer.draw_line_coloured(cx, y + 4, cx, y + h - 4, 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_coloured(self._debug_state, rx, ty, scale, theme.accent)
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_coloured(lsp_label, rx, ty, scale, lsp_colour)
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_coloured(lint_label, rx, ty, scale, lint_colour)
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_coloured(text, rx, ty, scale, colour)
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_coloured(bm_text, rx, ty, scale, theme.accent)
rx -= _PAD * 2
# Encoding
if self._encoding:
ew = renderer.text_width(self._encoding, scale)
rx -= ew
renderer.draw_text_coloured(self._encoding, rx, ty, scale, theme.text)
rx -= _PAD * 2
# Language
if self._language:
lw = renderer.text_width(self._language, scale)
rx -= lw
renderer.draw_text_coloured(self._language, rx, ty, scale, theme.text)
rx -= _PAD * 2
# Cursor position
cw = renderer.text_width(self._cursor_text, scale)
rx -= cw
renderer.draw_text_coloured(self._cursor_text, rx, ty, scale, theme.text)