Skip to content

Commit 7a98043

Browse files
committed
Show real document, Git and compiler status with optional monitoring
1 parent 236be24 commit 7a98043

7 files changed

Lines changed: 143 additions & 25 deletions

File tree

101 KB
Loading
106 KB
Loading
102 KB
Loading
107 KB
Loading

‎medha/app.py‎

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import os
22
import sys
3-
import psutil
4-
from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QFileDialog, QMessageBox, QLabel, QStatusBar, QGraphicsOpacityEffect
3+
from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QTabWidget, QFileDialog, QMessageBox, QGraphicsOpacityEffect
54
from PySide6.QtGui import QAction, QIcon, QKeySequence, QTextCursor
65
from PySide6.QtCore import Qt, QPropertyAnimation
76

@@ -625,22 +624,8 @@ def set_active_build_target(self, target):
625624
self.lbl_status.setText(f"Active Build Target: {target}")
626625

627626
def create_status_bar(self):
628-
status = QStatusBar()
629-
status.setStyleSheet("background-color: #2b2d30; color: #bcbec4; border-top: 1px solid #393b40;")
630-
self.setStatusBar(status)
631-
632-
cores = os.cpu_count() or "Unavailable"
633-
ram_gb = round(psutil.virtual_memory().total / (1024 ** 3), 1)
634-
635-
self.lbl_status = QLabel(f"Ready [{self.active_build_config}]")
636-
self.lbl_cursor_pos = QLabel("Ln 1, Col 1 | C")
637-
self.lbl_cursor_pos.setStyleSheet("color: #a1a3ab; margin-right: 15px;")
638-
self.lbl_sys_info = QLabel(f" CPU: {cores} Cores | RAM: {ram_gb} GB (Live OS) ")
639-
self.lbl_sys_info.setStyleSheet("color: #58a6ff; font-weight: bold;")
640-
641-
status.addWidget(self.lbl_status, 1)
642-
status.addPermanentWidget(self.lbl_cursor_pos)
643-
status.addPermanentWidget(self.lbl_sys_info)
627+
from medha.status import StatusController
628+
self.status_controller = StatusController(self)
644629

645630
def confirm_close(self, widget):
646631
if not isinstance(widget, AdvancedCodeEditorWidget) or not widget.is_modified:
@@ -670,6 +655,7 @@ def close_tab(self, index):
670655
self.show_welcome()
671656

672657
def on_tab_changed(self, index):
658+
self.update_cursor_status()
673659
editor = self.get_current_editor()
674660
if editor and hasattr(editor, 'get_symbol_outline'):
675661
symbols = editor.get_symbol_outline()
@@ -687,13 +673,8 @@ def update_tab_title(self, editor):
687673
break
688674

689675
def update_cursor_status(self):
690-
editor = self.get_current_editor()
691-
if editor:
692-
cursor = editor.textCursor()
693-
line = cursor.blockNumber() + 1
694-
col = cursor.columnNumber() + 1
695-
lang = getattr(editor, 'lang', 'cpp').upper()
696-
self.lbl_cursor_pos.setText(f"Ln {line}, Col {col} | {lang}")
676+
if hasattr(self, 'status_controller'):
677+
self.status_controller.document_changed()
697678

698679
def apply_dark_theme(self):
699680
self.apply_theme(self.workbench.prefs.value('theme', 'dark'))
@@ -768,6 +749,7 @@ def open_file_path(self, path):
768749

769750
filename = os.path.basename(path)
770751
editor = AdvancedCodeEditorWidget(file_path=path, parent=self)
752+
editor.encoding_label = 'UTF-8 (replacement)' if '\ufffd' in content else 'UTF-8'
771753
editor.setPlainText(content)
772754
editor.document().setModified(False)
773755
editor.is_modified = False
@@ -807,6 +789,8 @@ def action_open_folder(self):
807789
self.workspace_path = folder
808790
self.explorer.set_workspace(folder)
809791
self.terminal.set_working_directory(folder)
792+
self.workspace_ctrl = HybridWorkspaceController(folder)
793+
self.status_controller.refresh()
810794
self.lbl_status.setText(f"Opened workspace: {folder}")
811795

812796
def action_save_file(self):
@@ -1030,6 +1014,7 @@ def on_runner_finished(self, exit_code, diagnostics):
10301014
self.turbo_panel.on_finished(exit_code)
10311015
self._turbo_active = False
10321016
self.problems.set_diagnostics(diagnostics)
1017+
self.status_controller.set_diagnostics(diagnostics)
10331018
if diagnostics:
10341019
self.bottom_tabs.setCurrentWidget(self.problems)
10351020
self.lbl_status.setText(f"Build Finished with {len(diagnostics)} diagnostic error(s).")

‎medha/status.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Status values come from the active document, compiler discovery and local Git."""
2+
import os
3+
import psutil
4+
from PySide6.QtCore import QObject, QProcess, QTimer
5+
from PySide6.QtWidgets import QStatusBar, QLabel
6+
from PySide6.QtGui import QAction
7+
from medha import toolchains
8+
9+
10+
class StatusController(QObject):
11+
def __init__(self, window):
12+
super().__init__(window)
13+
self.window = window
14+
bar = QStatusBar()
15+
window.setStatusBar(bar)
16+
self.branch = QLabel('Git: checking…')
17+
self.diagnostics = QLabel('No build diagnostics')
18+
window.lbl_status = QLabel('Ready')
19+
window.lbl_cursor_pos = QLabel('No active file')
20+
self.compiler = QLabel('Compiler: —')
21+
window.lbl_theme = QLabel('Medhā Dark')
22+
window.lbl_sys_info = QLabel()
23+
for label in [self.branch, self.diagnostics]:
24+
bar.addWidget(label)
25+
bar.addWidget(window.lbl_status, 1)
26+
for label in [window.lbl_cursor_pos, self.compiler, window.lbl_theme, window.lbl_sys_info]:
27+
bar.addPermanentWidget(label)
28+
self.monitor = QAction('Show CPU and RAM', window)
29+
self.monitor.setCheckable(True)
30+
self.monitor.setChecked(window.workbench.prefs.value('system_monitor', False, type=bool))
31+
self.monitor.toggled.connect(self.toggle_monitor)
32+
window.menuBar().actions()[3].menu().addAction(self.monitor)
33+
self.toggle_monitor(self.monitor.isChecked())
34+
self.git = QProcess(self)
35+
self.git.finished.connect(self.branch_finished)
36+
self.git.errorOccurred.connect(lambda _: self.branch.setText('Git unavailable'))
37+
self.timer = QTimer(self)
38+
self.timer.timeout.connect(self.refresh)
39+
self.timer.start(5000)
40+
self.refresh()
41+
# Git is a short, local asynchronous command; no fetch or network is involved.
42+
43+
def toggle_monitor(self, enabled):
44+
self.window.lbl_sys_info.setVisible(enabled)
45+
self.window.workbench.prefs.setValue('system_monitor', enabled)
46+
47+
def refresh(self):
48+
if self.git.state() == QProcess.ProcessState.NotRunning:
49+
self.git.setWorkingDirectory(self.window.workspace_path)
50+
self.git.start('git', ['rev-parse', '--abbrev-ref', 'HEAD'])
51+
if self.monitor.isChecked():
52+
self.window.lbl_sys_info.setText(f'CPU {psutil.cpu_percent():.0f}% · RAM {psutil.virtual_memory().percent:.0f}%')
53+
self.document_changed()
54+
55+
def branch_finished(self, code, status):
56+
text = bytes(self.git.readAllStandardOutput()).decode('utf-8', 'replace').strip()
57+
self.branch.setText(('Branch: ' + text) if code == 0 and text else 'No Git repository')
58+
59+
def document_changed(self):
60+
window = self.window
61+
editor = window.get_current_editor()
62+
name = os.path.basename(editor.file_path) if editor and editor.file_path else ('Untitled' if editor else 'Welcome')
63+
window.setWindowTitle(f'Medhā · {name} · {os.path.basename(window.workspace_path)}')
64+
if not editor:
65+
window.lbl_cursor_pos.setText('No active file')
66+
self.compiler.setText('Compiler: —')
67+
return
68+
cursor = editor.textCursor()
69+
encoding = getattr(editor, 'encoding_label', 'UTF-8')
70+
window.lbl_cursor_pos.setText(f'Ln {cursor.blockNumber()+1}, Col {cursor.columnNumber()+1} · {editor.lang.upper()} · {encoding}')
71+
tool_name = {'c': 'gcc', 'cpp': 'g++', 'java': 'javac', 'python': 'python'}.get(editor.lang)
72+
tool = toolchains.find(tool_name) if tool_name else None
73+
self.compiler.setText(f'{tool_name}: ' + ('ready' if tool.found else 'not found') if tool else 'Compiler: —')
74+
self.compiler.setToolTip(tool.path or '' if tool else '')
75+
# Discovery respects bundled/user-configured compilers, not just PATH.
76+
77+
def set_diagnostics(self, diagnostics):
78+
errors = sum(d.severity == 'error' for d in diagnostics)
79+
warnings = sum(d.severity == 'warning' for d in diagnostics)
80+
self.diagnostics.setText(f'{errors} errors · {warnings} warnings')

‎tests/test_ui_smoke.py‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,3 +151,56 @@ def test_cancel_keeps_unsaved_tab(window, monkeypatch):
151151
monkeypatch.setattr(QMessageBox, 'question', lambda *args: QMessageBox.StandardButton.Discard)
152152
window.close_tab(window.tab_widget.indexOf(editor))
153153
assert window.tab_widget.indexOf(editor) == -1
154+
155+
156+
def test_status_uses_document_and_build_data(window, tmp_path):
157+
from types import SimpleNamespace
158+
source = tmp_path / 'sample.py'
159+
source.write_text('print(1)\nprint(2)\n')
160+
window.open_file_path(str(source))
161+
window.navigate_to_line(2)
162+
assert 'Ln 2' in window.lbl_cursor_pos.text()
163+
assert 'PYTHON' in window.lbl_cursor_pos.text()
164+
assert 'UTF-8' in window.lbl_cursor_pos.text()
165+
assert 'sample.py' in window.windowTitle()
166+
assert window.lbl_sys_info.isHidden()
167+
window.status_controller.set_diagnostics([SimpleNamespace(severity='error'), SimpleNamespace(severity='warning')])
168+
assert window.status_controller.diagnostics.text() == '1 errors · 1 warnings'
169+
window.status_controller.monitor.setChecked(True)
170+
window.status_controller.refresh()
171+
assert '%' in window.lbl_sys_info.text()
172+
173+
174+
def test_every_menu_action_from_welcome(window, monkeypatch):
175+
"""Exercise UI slots with cancelled dialogs and isolated external build calls."""
176+
import sys
177+
from PySide6.QtWidgets import QDialog, QFileDialog, QMessageBox, QInputDialog, QMenu
178+
from medha.command_palette import collect_actions
179+
from medha.build import BuildSystem
180+
errors = []
181+
monkeypatch.setattr(sys, 'excepthook', lambda *error: errors.append(error))
182+
monkeypatch.setattr(QDialog, 'exec', lambda self: QDialog.DialogCode.Rejected)
183+
monkeypatch.setattr(QMessageBox, 'exec', lambda self: QMessageBox.StandardButton.Cancel)
184+
monkeypatch.setattr(QMenu, 'exec', lambda *args: None)
185+
monkeypatch.setattr(QFileDialog, 'getOpenFileName', lambda *a, **k: ('', ''))
186+
monkeypatch.setattr(QFileDialog, 'getSaveFileName', lambda *a, **k: ('', ''))
187+
monkeypatch.setattr(QFileDialog, 'getExistingDirectory', lambda *a, **k: '')
188+
monkeypatch.setattr(QInputDialog, 'getInt', lambda *a, **k: (1, False))
189+
monkeypatch.setattr(BuildSystem, 'configure', lambda self: (0, 'Smoke configuration'))
190+
monkeypatch.setattr(BuildSystem, 'build', lambda self: (0, 'Smoke build', None))
191+
monkeypatch.setattr(window, 'open_lab_program', lambda path: None)
192+
# Each action starts in the legitimate no-active-document Welcome state.
193+
# Editing/build behavior with a real document is covered in the focused tests.
194+
actions = [action for _, action in collect_actions(window.menuBar())]
195+
exit_action = next(a for a in actions if a.text().replace('&', '') == 'Exit')
196+
for action in actions:
197+
if action == exit_action:
198+
continue
199+
window.show_welcome()
200+
action.trigger()
201+
QApplication.processEvents()
202+
assert not errors, (action.text(), errors)
203+
assert window.isVisible(), action.text()
204+
exit_action.trigger()
205+
assert not errors
206+
assert not window.isVisible()

0 commit comments

Comments
 (0)