Skip to content

Commit 7c21c05

Browse files
author
PyCompiler ARK++
committed
feat: Implement BCASL Studio application with UI and core functionality
- Added main application logic in app.py to initialize the BCASL Studio. - Created configuration management in config.py to handle YAML and JSON formats. - Developed plugin discovery and management in plugins.py. - Implemented report summarization in report.py for plugin execution results. - Added runner functionality in runner.py to execute BCASL with specified configurations. - Designed the main window UI in studio.ui and integrated it with the application. - Enhanced user experience with workspace and plugin directory selection. - Implemented theme application based on user preferences. - Added logging and error handling throughout the application for better debugging.
1 parent dc34035 commit 7c21c05

9 files changed

Lines changed: 987 additions & 0 deletions

File tree

bcasl/only_mod/app.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
from __future__ import annotations
3+
4+
import os
5+
import sys
6+
from pathlib import Path
7+
8+
from PySide6.QtWidgets import QApplication
9+
from PySide6.QtGui import QIcon
10+
11+
from .widgets.main_window import BCASLStudioWindow
12+
13+
14+
APP_NAME = "BCASL Studio"
15+
16+
17+
def _apply_theme(app: QApplication) -> None:
18+
"""Load a QSS theme from the repo's themes/ directory.
19+
Priority: env BCASL_THEME -> dark.qss -> light.qss. Safe no-op on errors.
20+
"""
21+
try:
22+
root = Path(__file__).resolve().parents[2]
23+
themes_dir = root / "themes"
24+
if not themes_dir.is_dir():
25+
return
26+
chosen = os.environ.get("BCASL_THEME", "").strip()
27+
candidates = [chosen] if chosen else ["dark.qss", "light.qss"]
28+
for name in candidates:
29+
if not name:
30+
continue
31+
p = themes_dir / name
32+
if p.is_file():
33+
qss = p.read_text(encoding="utf-8")
34+
app.setStyleSheet(qss)
35+
return
36+
except Exception:
37+
pass
38+
39+
40+
def run(argv: list[str]) -> int:
41+
# Minimal environment sanity
42+
os.environ.setdefault("PYTHONUTF8", "1")
43+
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
44+
45+
app = QApplication(argv)
46+
app.setApplicationName(APP_NAME)
47+
# Try to reuse repo logo if present
48+
try:
49+
root = Path(__file__).resolve().parents[2]
50+
icon = root / "logo" / "logo.png"
51+
if icon.is_file():
52+
app.setWindowIcon(QIcon(str(icon)))
53+
except Exception:
54+
pass
55+
56+
_apply_theme(app)
57+
58+
w = BCASLStudioWindow()
59+
w.show()
60+
rc = app.exec()
61+
return int(rc) if isinstance(rc, int) else 0

bcasl/only_mod/core/config.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
from __future__ import annotations
3+
4+
from dataclasses import dataclass, field
5+
from pathlib import Path
6+
from typing import Any
7+
import json
8+
import yaml
9+
10+
DEFAULT_EXCLUDE = [
11+
"**/__pycache__/**",
12+
"**/*.pyc",
13+
".git/**",
14+
"venv/**",
15+
".venv/**",
16+
]
17+
18+
DEFAULT_INCLUDE = ["**/*.py"]
19+
20+
21+
@dataclass
22+
class BCASLConfig:
23+
file_patterns: list[str] = field(default_factory=lambda: list(DEFAULT_INCLUDE))
24+
exclude_patterns: list[str] = field(default_factory=lambda: list(DEFAULT_EXCLUDE))
25+
options: dict[str, Any] = field(
26+
default_factory=lambda: {
27+
"plugin_timeout_s": 0.0,
28+
"sandbox": True,
29+
"plugin_parallelism": 0,
30+
"iter_files_cache": True,
31+
"env": {},
32+
}
33+
)
34+
plugins: dict[str, Any] = field(default_factory=dict)
35+
plugin_order: list[str] = field(default_factory=list)
36+
37+
def to_dict(self) -> dict[str, Any]:
38+
d = {
39+
"file_patterns": list(self.file_patterns),
40+
"exclude_patterns": list(self.exclude_patterns),
41+
"options": dict(self.options),
42+
"plugins": dict(self.plugins),
43+
"plugin_order": list(self.plugin_order),
44+
}
45+
# Never persist global 'enabled' flag; only per-plugin enabled
46+
try:
47+
d["options"].pop("enabled", None)
48+
except Exception:
49+
pass
50+
return d
51+
52+
53+
def read_yaml(path: Path) -> dict[str, Any]:
54+
try:
55+
return yaml.safe_load(path.read_text(encoding="utf-8")) or {}
56+
except Exception:
57+
return {}
58+
59+
60+
def write_yaml(path: Path, data: dict[str, Any]) -> None:
61+
path.write_text(yaml.safe_dump(data, sort_keys=False, allow_unicode=True), encoding="utf-8")
62+
63+
64+
def read_json(path: Path) -> dict[str, Any]:
65+
try:
66+
return json.loads(path.read_text(encoding="utf-8"))
67+
except Exception:
68+
return {}
69+
70+
71+
def load_config(workspace: Path) -> BCASLConfig:
72+
"""Load bcasl.yml if present; migrate legacy JSON to YAML; otherwise return defaults."""
73+
yml = workspace / "bcasl.yml"
74+
legacy = [workspace / "bcasl.json", workspace / ".bcasl.json"]
75+
data: dict[str, Any] = {}
76+
if yml.is_file():
77+
data = read_yaml(yml)
78+
else:
79+
# migrate JSON if exists
80+
for p in legacy:
81+
if p.is_file():
82+
data = read_json(p)
83+
break
84+
if data:
85+
# persist migration
86+
try:
87+
write_yaml(yml, data)
88+
except Exception:
89+
pass
90+
cfg = BCASLConfig()
91+
if isinstance(data, dict) and data:
92+
cfg.file_patterns = list(data.get("file_patterns", cfg.file_patterns))
93+
cfg.exclude_patterns = list(data.get("exclude_patterns", cfg.exclude_patterns))
94+
opts = dict(data.get("options", {})) if isinstance(data.get("options"), dict) else {}
95+
# ensure no persisted global enabled
96+
opts.pop("enabled", None)
97+
cfg.options.update(opts)
98+
cfg.plugins = dict(data.get("plugins", {}))
99+
cfg.plugin_order = list(data.get("plugin_order", []))
100+
return cfg
101+
102+
103+
def save_config(workspace: Path, cfg: BCASLConfig) -> None:
104+
yml = workspace / "bcasl.yml"
105+
write_yaml(yml, cfg.to_dict())

bcasl/only_mod/core/plugins.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
from __future__ import annotations
3+
4+
from pathlib import Path
5+
from typing import Any
6+
7+
from bcasl.tagging import compute_tag_order
8+
9+
10+
def discover_plugins(plugins_dir: Path) -> dict[str, dict[str, Any]]:
11+
"""Return meta mapping id -> {name, version, description, author, tags}."""
12+
meta: dict[str, dict[str, Any]] = {}
13+
try:
14+
import importlib.util as _ilu
15+
import sys as _sys
16+
17+
for pkg_dir in sorted(plugins_dir.iterdir(), key=lambda p: p.name):
18+
try:
19+
if not pkg_dir.is_dir():
20+
continue
21+
init_py = pkg_dir / "__init__.py"
22+
if not init_py.exists():
23+
continue
24+
mod_name = f"bcasl_meta_only_{pkg_dir.name}"
25+
spec = _ilu.spec_from_file_location(
26+
mod_name, str(init_py), submodule_search_locations=[str(pkg_dir)]
27+
)
28+
if spec is None or spec.loader is None:
29+
continue
30+
module = _ilu.module_from_spec(spec)
31+
_sys.modules[mod_name] = module
32+
spec.loader.exec_module(module) # type: ignore[attr-defined]
33+
reg = getattr(module, "bcasl_register", None)
34+
if not callable(reg):
35+
continue
36+
# lightweight registration to extract meta
37+
from bcasl.executor import BCASL
38+
39+
mgr = BCASL(plugins_dir, config={}, sandbox=False, plugin_timeout_s=0.0)
40+
reg(mgr)
41+
for pid, rec in getattr(mgr, "_registry", {}).items():
42+
try:
43+
plg = rec.plugin
44+
tags = []
45+
try:
46+
tags0 = getattr(plg.meta, "tags", ())
47+
tags = [str(x).strip().lower() for x in tags0 if str(x).strip()]
48+
except Exception:
49+
tags = []
50+
meta[plg.meta.id] = {
51+
"id": plg.meta.id,
52+
"name": plg.meta.name,
53+
"version": plg.meta.version,
54+
"description": plg.meta.description,
55+
"author": plg.meta.author,
56+
"tags": tags,
57+
}
58+
except Exception:
59+
continue
60+
except Exception:
61+
continue
62+
except Exception:
63+
pass
64+
return meta
65+
66+
67+
def compute_initial_order(meta_map: dict[str, dict[str, Any]]) -> list[str]:
68+
try:
69+
return list(compute_tag_order(meta_map))
70+
except Exception:
71+
return sorted(meta_map.keys())

bcasl/only_mod/core/report.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
from __future__ import annotations
3+
4+
from dataclasses import dataclass
5+
from typing import Any, Iterable
6+
7+
8+
@dataclass
9+
class PluginResult:
10+
plugin_id: str
11+
success: bool
12+
error: str
13+
duration_ms: float
14+
15+
16+
def summarize(report: Iterable[Any]) -> dict[str, Any]:
17+
ok = 0
18+
fail = 0
19+
total_ms = 0.0
20+
for item in report:
21+
ok += 1 if getattr(item, "success", False) else 0
22+
fail += 0 if getattr(item, "success", False) else 1
23+
total_ms += float(getattr(item, "duration_ms", 0.0))
24+
return {"ok": ok, "fail": fail, "total_ms": total_ms}

bcasl/only_mod/core/runner.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
from __future__ import annotations
3+
4+
from pathlib import Path
5+
from typing import Any, Optional
6+
7+
from bcasl.executor import BCASL
8+
from bcasl.Base import PreCompileContext
9+
10+
11+
def run_bcasl(workspace: Path, plugins_dir: Path, cfg: dict[str, Any]) -> Optional[object]:
12+
"""Synchronous run of BCASL with given config dict (already merged)."""
13+
try:
14+
manager = BCASL(workspace, config=cfg, plugin_timeout_s=float(cfg.get("options", {}).get("plugin_timeout_s", 0.0)))
15+
loaded, errors = manager.load_plugins_from_directory(plugins_dir)
16+
# apply enables/priorities
17+
pmap = cfg.get("plugins", {}) if isinstance(cfg, dict) else {}
18+
if isinstance(pmap, dict):
19+
for pid, val in pmap.items():
20+
try:
21+
enabled = (
22+
val if isinstance(val, bool) else bool((val or {}).get("enabled", True))
23+
)
24+
if not enabled:
25+
manager.disable_plugin(pid)
26+
except Exception:
27+
pass
28+
try:
29+
if isinstance(val, dict) and "priority" in val:
30+
manager.set_priority(pid, int(val.get("priority", 0)))
31+
except Exception:
32+
pass
33+
order_list = list(cfg.get("plugin_order", [])) if isinstance(cfg, dict) else []
34+
if order_list:
35+
for idx, pid in enumerate(order_list):
36+
try:
37+
manager.set_priority(pid, int(idx))
38+
except Exception:
39+
pass
40+
report = manager.run_pre_compile(PreCompileContext(workspace, config=cfg))
41+
return report
42+
except Exception:
43+
return None

bcasl/only_mod/main.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
from __future__ import annotations
3+
4+
import sys
5+
from .app import run
6+
7+
8+
if __name__ == "__main__":
9+
sys.exit(run(sys.argv))

0 commit comments

Comments
 (0)