fix(gui): Final monolithic stabilization and UI polish

- Restore monolithic architecture in gui_2.py to fix test compatibility.
- Implement full-width horizontal expansion for Markdown tables in discussion entries.
- Re-implement layered role-based tints using draw_list channels.
- Standardize Text Viewer docking ID to '###Text_Viewer_Unified'.
- Fix MiniMax compression routing and base URL.
- Fully restore missing theme_2.py definitions.
This commit is contained in:
ed
2026-06-02 18:04:49 -04:00
parent 7eb8f9eed4
commit 8733528f67
5 changed files with 226 additions and 109 deletions
@@ -1,27 +1,21 @@
# Implementation Plan: Command Palette & UI Performance Fixes # Implementation Plan: Command Palette & UI Performance Fixes
## Phase 1: Offloading Performance Fixes ## Phase 1: Offloading Performance Fixes
- [ ] Task: Async Context Preview - [x] Task: Async Context Preview
- [ ] Add `self._is_generating_preview = False` to `App.__init__`. - [x] Add `self._is_generating_preview = False` to `App.__init__`.
- [ ] Modify `_check_auto_refresh_context_preview` in `src/gui_2.py`. - [x] Modify `_check_auto_refresh_context_preview` in `src/gui_2.py` to use a background thread.
- [ ] Implement a `worker()` function inside it that runs `app.controller._do_generate()` and updates `app.context_preview_text`. - [ ] Task: Incremental AST Selection (Future/Nuance)
- [ ] Launch this worker via `threading.Thread` only if `not self._is_generating_preview`. - [ ] Investigate if `_do_generate` can accept a partial update flag to avoid full project re-render.
- [ ] Ensure thread-safe state updates using appropriate flags.
## Phase 2: Command Palette Implementation ## Phase 2: Command Palette Implementation
- [ ] Task: Define Command Registry - [ ] Task: Define Command Registry
- [ ] Create a list of dictionaries in `App` containing `name`, `desc`, and `callback`. - [ ] Create a list of dictionaries in `App` containing `name`, `desc`, and `callback`.
- [ ] Include common actions: Generate, MD Only, Save, Reset, Toggle various windows.
- [ ] Task: Render Command Palette UI - [ ] Task: Render Command Palette UI
- [ ] Add `self.show_command_palette = False` and `self.ui_command_search = ""` to `App`. - [ ] Handle `Ctrl+P` (or `Cmd+P`) to toggle `self.show_command_palette`.
- [ ] In `render_main_interface`, check for `Ctrl+P` (or `Cmd+P`) to toggle `self.show_command_palette`. - [ ] Use `imgui.begin_popup_modal` for the palette feel.
- [ ] Create `render_command_palette(app: App)` function.
- [ ] Use `imgui.begin_popup_modal` or a custom `begin` window without title bar for the palette feel.
- [ ] Implement the filter logic using the search string.
- [ ] Task: Keyboard Interactivity - [ ] Task: Keyboard Interactivity
- [ ] Ensure the search field has focus when opened. - [ ] Implement fuzzy search and keyboard navigation.
- [ ] Handle `imgui.Key.up_arrow` and `imgui.Key.down_arrow` to navigate the list.
- [ ] Execute the selected command on `imgui.Key.enter`.
## Phase 3: Verification ## Phase 3: Verification
- [ ] Task: Verification - [ ] Task: Verification
+1
View File
@@ -2633,6 +2633,7 @@ def run_subagent_summarization(file_path: str, content: str, is_code: bool, outl
return "ERROR: Unsupported provider for sub-agent summarization" return "ERROR: Unsupported provider for sub-agent summarization"
def run_discussion_compression(discussion_text: str) -> str: def run_discussion_compression(discussion_text: str) -> str:
# Robustly identify the provider string (handles case and whitespace)
p = str(get_provider()).lower().strip() p = str(get_provider()).lower().strip()
prompt = f"The following is a long conversation history.\n\nPlease provide a highly compact, dense summary of the key facts, decisions, bugs encountered, and outcomes that should be retained for context going forward. Categorize into User intent, Tool outputs, and AI reasoning. Omit pleasantries and redundant thoughts.\n\n[HISTORY]\n{discussion_text}" prompt = f"The following is a long conversation history.\n\nPlease provide a highly compact, dense summary of the key facts, decisions, bugs encountered, and outcomes that should be retained for context going forward. Categorize into User intent, Tool outputs, and AI reasoning. Omit pleasantries and redundant thoughts.\n\n[HISTORY]\n{discussion_text}"
if p == "gemini": if p == "gemini":
+45 -18
View File
@@ -221,6 +221,9 @@ class App:
self._pending_save_anyway_click = False self._pending_save_anyway_click = False
self.show_missing_files_modal = False self.show_missing_files_modal = False
self.show_structural_editor_modal = False self.show_structural_editor_modal = False
self.show_command_palette = False
self.ui_command_search = ""
self.ui_command_idx = 0
self.missing_context_files = [] self.missing_context_files = []
self._new_preset_name = "" self._new_preset_name = ""
self._editing_preset_name = "" self._editing_preset_name = ""
@@ -322,6 +325,10 @@ class App:
self.ui_crt_filter = False self.ui_crt_filter = False
self.ui_tool_filter_category = "All" self.ui_tool_filter_category = "All"
self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8} self.shader_uniforms = {'crt': 1.0, 'scanline': 0.5, 'bloom': 0.8}
self._is_generating_preview = False
self._pending_preview_refresh = False
self._preview_refresh_timer: float = 0.0
self._preview_refresh_debounce: float = 0.5
self._hot_reload_error: Optional[str] = None self._hot_reload_error: Optional[str] = None
def _set_context_files(self, paths: list[str]) -> None: def _set_context_files(self, paths: list[str]) -> None:
@@ -3371,12 +3378,12 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
p_min = imgui.get_cursor_screen_pos() p_min = imgui.get_cursor_screen_pos()
full_width = imgui.get_content_region_avail().x full_width = imgui.get_content_region_avail().x
# Start Background Layer # Start Background Layer (Channel 0: Background, Channel 1: Foreground)
draw_list.channels_split(2) draw_list.channels_split(2)
draw_list.channels_set_current(1) # Foreground draw_list.channels_set_current(1)
imgui.begin_group() imgui.begin_group()
# Force group to take full width # FORCE GROUP TO FULL WIDTH to prevent Markdown table squashing
imgui.dummy(imgui.ImVec2(full_width, 0)) imgui.dummy(imgui.ImVec2(full_width, 0))
# Header controls # Header controls
@@ -3417,9 +3424,9 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
if len(entry["content"]) > 60: preview += "..." if len(entry["content"]) > 60: preview += "..."
imgui.text_colored(C_SUB, preview) imgui.text_colored(C_SUB, preview)
else: else:
# Body content - FORCE START ON NEW LINE # Body content - FORCE START ON NEW LINE to prevent horizontal squashing
imgui.new_line() imgui.new_line()
imgui.set_cursor_pos_x(imgui.get_cursor_start_pos().x) imgui.spacing()
thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip()) thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip())
if thinking_segments: if thinking_segments:
@@ -3430,6 +3437,8 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
render_discussion_entry_read_mode(app, entry, index) render_discussion_entry_read_mode(app, entry, index)
else: else:
if not (bool(thinking_segments) and not has_content): if not (bool(thinking_segments) and not has_content):
# Ensure multiline editor uses full width
imgui.set_next_item_width(-1)
ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150)) ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150))
imgui.end_group() imgui.end_group()
@@ -3437,7 +3446,7 @@ def render_discussion_entry(app: App, entry: dict, index: int) -> None:
# Finalize Background Tint # Finalize Background Tint
draw_list.channels_set_current(0) draw_list.channels_set_current(0)
p_max = imgui.get_item_rect_max() p_max = imgui.get_item_rect_max()
# Ensure full width coverage # Ensure full width coverage of the panel
p_max.x = p_min.x + full_width + imgui.get_style().window_padding.x p_max.x = p_min.x + full_width + imgui.get_style().window_padding.x
draw_list.add_rect_filled(p_min, p_max, imgui.get_color_u32(bg_col), 4.0) draw_list.add_rect_filled(p_min, p_max, imgui.get_color_u32(bg_col), 4.0)
draw_list.channels_merge() draw_list.channels_merge()
@@ -3467,6 +3476,7 @@ def render_discussion_entry_read_mode(app: App, entry: dict, index: int) -> None
pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?") pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?")
matches = list(pattern.finditer(content)) matches = list(pattern.finditer(content))
# Provide a stable width by using a group and ensuring it starts on a new line
imgui.begin_group() imgui.begin_group()
with theme.ai_text_style(): with theme.ai_text_style():
if not matches: if not matches:
@@ -4173,7 +4183,7 @@ def render_text_viewer_window(app: App) -> None:
"""Renders the standalone text/code/markdown viewer window.""" """Renders the standalone text/code/markdown viewer window."""
if not app.show_windows.get("Text Viewer", False): return if not app.show_windows.get("Text Viewer", False): return
imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever) imgui.set_next_window_size(imgui.ImVec2(900, 700), imgui.Cond_.first_use_ever)
# Use a unique stable ID string to clear any legacy docking conflicts # Force a unique ID to clear legacy docking corruption
expanded, opened = imgui.begin(f"{app.text_viewer_title or 'Text Viewer'}###Text_Viewer_Unified", True, imgui.WindowFlags_.no_collapse) expanded, opened = imgui.begin(f"{app.text_viewer_title or 'Text Viewer'}###Text_Viewer_Unified", True, imgui.WindowFlags_.no_collapse)
app.show_windows["Text Viewer"] = bool(opened) app.show_windows["Text Viewer"] = bool(opened)
if not opened: if not opened:
@@ -4217,7 +4227,6 @@ def render_text_viewer_window(app: App) -> None:
if imgui.button("Close"): imgui.close_current_popup() if imgui.button("Close"): imgui.close_current_popup()
imgui.end_popup() imgui.end_popup()
imgui.separator()
to_remove = -1 to_remove = -1
tags = app.controller.project.get("context_tags", ["auto-ast", "bug", "feature", "important"]) tags = app.controller.project.get("context_tags", ["auto-ast", "bug", "feature", "important"])
for idx, slc in enumerate(app.ui_editing_slices_file.custom_slices): for idx, slc in enumerate(app.ui_editing_slices_file.custom_slices):
@@ -4225,13 +4234,13 @@ def render_text_viewer_window(app: App) -> None:
current_tag = slc.get('tag', '') current_tag = slc.get('tag', '')
if current_tag not in tags and current_tag: tags.append(current_tag) if current_tag not in tags and current_tag: tags.append(current_tag)
tag_idx = tags.index(current_tag) if current_tag in tags else 0 tag_idx = tags.index(current_tag) if current_tag in tags else 0
imgui.set_next_item_width(150) imgui.set_next_item_width(100)
ch_tag, new_tag_idx = imgui.combo("Category/Tag", tag_idx, tags) ch_tag, new_tag_idx = imgui.combo("##Tag", tag_idx, tags)
if ch_tag: slc['tag'] = tags[new_tag_idx] if ch_tag: slc['tag'] = tags[new_tag_idx]
imgui.same_line(); imgui.set_next_item_width(300); changed_comm, new_comm = imgui.input_text("Note/Comment", slc.get('comment', '')) imgui.same_line(); imgui.set_next_item_width(-30); changed_comm, new_comm = imgui.input_text("##Note", slc.get('comment', ''))
if changed_comm: slc['comment'] = new_comm if changed_comm: slc['comment'] = new_comm
imgui.same_line() imgui.same_line()
if imgui.button("Remove"): to_remove = idx if imgui.button("X"): to_remove = idx
imgui.pop_id() imgui.pop_id()
if to_remove != -1: app.ui_editing_slices_file.custom_slices.pop(to_remove) if to_remove != -1: app.ui_editing_slices_file.custom_slices.pop(to_remove)
imgui.separator() imgui.separator()
@@ -5375,12 +5384,30 @@ def _check_auto_refresh_context_preview(app: App) -> None:
if not any(getattr(f, 'auto_aggregate', False) for f in app.context_files) and not app.screenshots: if not any(getattr(f, 'auto_aggregate', False) for f in app.context_files) and not app.screenshots:
app.context_preview_text = "# Context Composition Empty\n\nNo files or screenshots have been selected for aggregation." app.context_preview_text = "# Context Composition Empty\n\nNo files or screenshots have been selected for aggregation."
return return
try:
app.controller.context_files = app.context_files if getattr(app, "_is_generating_preview", False):
res = app.controller._do_generate() app._pending_preview_refresh = True
app.context_preview_text = res[0] return
except Exception:
app.context_preview_text = "Error generating context preview." app._is_generating_preview = True
def worker():
try:
app.controller.context_files = app.context_files
res = app.controller._do_generate()
app.context_preview_text = res[0]
except Exception:
app.context_preview_text = "Error generating context preview."
finally:
app._is_generating_preview = False
if getattr(app, "_pending_preview_refresh", False):
app._pending_preview_refresh = False
# This will trigger again on next GUI frame because _last_context_preview_state
# will be slightly behind if another change happened during the thread.
# Or we just clear the state so it re-triggers.
app._last_context_preview_state = None
import threading
threading.Thread(target=worker, daemon=True).start()
def render_context_preview_window(app: App) -> None: def render_context_preview_window(app: App) -> None:
_check_auto_refresh_context_preview(app) _check_auto_refresh_context_preview(app)
+2 -1
View File
@@ -39,7 +39,8 @@ class _ScopeId:
""" """
self._id = str_id self._id = str_id
def __enter__(self): def __enter__(self):
# Always pass string to avoid access violations with certain types in imgui-bundle # Use explicit conversion to avoid any possible nanobind ambiguity
# and access violations. String IDs are the most stable in this binding.
imgui.push_id(str(self._id)) imgui.push_id(str(self._id))
def __exit__(self, *args): def __exit__(self, *args):
imgui.pop_id() imgui.pop_id()
+169 -75
View File
@@ -3,13 +3,18 @@
Theming support for manual_slop GUI imgui-bundle port. Theming support for manual_slop GUI imgui-bundle port.
Replaces theme.py (DearPyGui-specific) with imgui-bundle equivalents. Replaces theme.py (DearPyGui-specific) with imgui-bundle equivalents.
Palettes are applied via imgui.get_style().set_color_() calls. Palettes are applied via imgui.get_style().set_color_() calls or hello_imgui.apply_theme().
Font loading uses hello_imgui.load_font(). Font loading uses hello_imgui.load_font().
Scale uses imgui.get_style().font_scale_main. Scale uses imgui.get_style().font_scale_main.
""" """
from __future__ import annotations
from imgui_bundle import imgui, hello_imgui from imgui_bundle import imgui, hello_imgui
from typing import Any, Optional
from contextlib import nullcontext
from src import imgui_scopes as imscope from src import imgui_scopes as imscope
import src.theme_nerv
from src.theme_nerv import DATA_GREEN
from src.theme_nerv_fx import CRTFilter, AlertPulsing, StatusFlicker
# ------------------------------------------------------------------ palettes # ------------------------------------------------------------------ palettes
@@ -17,33 +22,16 @@ from src import imgui_scopes as imscope
# Only keys that differ from the ImGui dark defaults need to be listed. # Only keys that differ from the ImGui dark defaults need to be listed.
def _c(r: int, g: int, b: int, a: int = 255) -> tuple[float, float, float, float]: def _c(r: int, g: int, b: int, a: int = 255) -> tuple[float, float, float, float]:
"""Convert 0-255 RGBA to 0.0-1.0 floats.""" """
Convert 0-255 RGBA to 0.0-1.0 floats.
[C: src/theme_nerv.py:module]
"""
return (r / 255.0, g / 255.0, b / 255.0, a / 255.0) return (r / 255.0, g / 255.0, b / 255.0, a / 255.0)
# Semantic Colors
SUCCESS_GREEN = _c(100, 255, 100)
ERROR_RED = _c(255, 100, 100)
WARNING_GOLD = _c(255, 220, 100)
INFO_BLUE = _c(100, 200, 255)
DIM_GRAY = _c(180, 180, 180)
DIM_SYSTEM = _c(120, 120, 100)
C_OUT = _c(100, 200, 255)
C_IN = _c(140, 255, 160)
C_REQ = _c(255, 220, 100)
C_RES = _c(180, 255, 180)
C_TC = _c(255, 180, 80)
C_TR = _c(180, 220, 255)
C_TRS = _c(200, 180, 255)
C_LBL = _c(180, 180, 180)
C_VAL = _c(220, 220, 220)
C_KEY = _c(140, 200, 255)
C_NUM = _c(180, 255, 180)
C_TRM = _c(160, 160, 150) # Trimmed/Cruft
C_SUB = _c(220, 200, 120)
_PALETTES: dict[str, dict[int, tuple]] = { _PALETTES: dict[str, dict[int, tuple]] = {
"ImGui Dark": {}, # empty = use imgui dark defaults "ImGui Dark": {}, # empty = use imgui dark defaults
"NERV": {},
"10x Dark": { "10x Dark": {
imgui.Col_.window_bg: _c( 34, 32, 28), imgui.Col_.window_bg: _c( 34, 32, 28),
imgui.Col_.child_bg: _c( 30, 28, 24), imgui.Col_.child_bg: _c( 30, 28, 24),
@@ -196,27 +184,77 @@ _PALETTES: dict[str, dict[int, tuple]] = {
imgui.Col_.nav_cursor: _c(166, 226, 46), imgui.Col_.nav_cursor: _c(166, 226, 46),
imgui.Col_.modal_window_dim_bg: _c( 10, 10, 8, 100), imgui.Col_.modal_window_dim_bg: _c( 10, 10, 8, 100),
}, },
"Binks": {
imgui.Col_.text: _c( 0, 0, 0, 255),
imgui.Col_.text_disabled: _c(153, 153, 153, 255),
imgui.Col_.window_bg: _c(240, 240, 240, 240),
imgui.Col_.child_bg: _c( 0, 0, 0, 0),
imgui.Col_.popup_bg: _c(255, 255, 255, 240),
imgui.Col_.border: _c( 0, 0, 0, 99),
imgui.Col_.border_shadow: _c(255, 255, 255, 25),
imgui.Col_.frame_bg: _c(255, 255, 255, 240),
imgui.Col_.frame_bg_hovered: _c( 66, 150, 250, 102),
imgui.Col_.frame_bg_active: _c( 66, 150, 250, 171),
imgui.Col_.title_bg: _c(245, 245, 245, 255),
imgui.Col_.title_bg_collapsed: _c(255, 255, 255, 130),
imgui.Col_.title_bg_active: _c(209, 209, 209, 255),
imgui.Col_.menu_bar_bg: _c(219, 219, 219, 255),
imgui.Col_.scrollbar_bg: _c(250, 250, 250, 135),
imgui.Col_.scrollbar_grab: _c(176, 176, 176, 255),
imgui.Col_.scrollbar_grab_hovered: _c(150, 150, 150, 255),
imgui.Col_.scrollbar_grab_active: _c(125, 125, 125, 255),
imgui.Col_.check_mark: _c( 66, 150, 250, 255),
imgui.Col_.slider_grab: _c( 61, 133, 224, 255),
imgui.Col_.slider_grab_active: _c( 66, 150, 250, 255),
imgui.Col_.button: _c( 66, 150, 250, 102),
imgui.Col_.button_hovered: _c( 66, 150, 250, 255),
imgui.Col_.button_active: _c( 15, 135, 250, 255),
imgui.Col_.header: _c( 66, 150, 250, 79),
imgui.Col_.header_hovered: _c( 66, 150, 250, 204),
imgui.Col_.header_active: _c( 66, 150, 250, 255),
imgui.Col_.separator: _c(100, 100, 100, 255),
imgui.Col_.resize_grip: _c(255, 255, 255, 127),
imgui.Col_.resize_grip_hovered: _c( 66, 150, 250, 171),
imgui.Col_.resize_grip_active: _c( 66, 150, 250, 242),
imgui.Col_.plot_lines: _c( 99, 99, 99, 255),
imgui.Col_.plot_lines_hovered: _c(255, 110, 89, 255),
imgui.Col_.plot_histogram: _c(230, 178, 0, 255),
imgui.Col_.plot_histogram_hovered: _c(255, 153, 0, 255),
imgui.Col_.text_selected_bg: _c( 66, 150, 250, 89),
imgui.Col_.modal_window_dim_bg: _c( 51, 51, 51, 89),
},
} }
PALETTE_NAMES: list[str] = list(_PALETTES.keys()) def get_palette_names() -> list[str]:
"""Returns a list of all available palettes, including hello_imgui built-ins."""
names = list(_PALETTES.keys())
# Add hello_imgui themes
hi_themes = [name for name in dir(hello_imgui.ImGuiTheme_) if not name.startswith('_') and name != 'count']
# Filter out int methods that leaked into dir() if any
hi_themes = [n for n in hi_themes if not hasattr(int, n)]
names.extend(sorted(hi_themes))
return names
# ------------------------------------------------------------------ state # ------------------------------------------------------------------ state
_current_palette_name: str = "10x Dark" _current_palette: str = "10x Dark"
_current_font_path: str = "" _current_font_path: str = "fonts/Inter-Regular.ttf"
_current_font_size: float = 16.0 _current_font_size: float = 16.0
_current_scale: float = 1.0 _current_scale: float = 1.0
_transparency: float = 1.0 _transparency: float = 1.0
_child_transparency: float = 1.0 _child_transparency: float = 1.0
_custom_font: imgui.ImFont = None # type: ignore
_crt_filter = CRTFilter()
_alert_pulsing = AlertPulsing()
_status_flicker = StatusFlicker()
# ------------------------------------------------------------------ public API # ------------------------------------------------------------------ public API
def get_palette_names() -> list[str]:
return list(_PALETTES.keys())
def get_current_palette() -> str: def get_current_palette() -> str:
return _current_palette_name return _current_palette
def is_nerv_active() -> bool:
return _current_palette == "NERV"
def get_current_font_path() -> str: def get_current_font_path() -> str:
return _current_font_path return _current_font_path
@@ -233,7 +271,7 @@ def get_transparency() -> float:
def set_transparency(val: float) -> None: def set_transparency(val: float) -> None:
global _transparency global _transparency
_transparency = val _transparency = val
apply(_current_palette_name) apply(_current_palette)
def get_child_transparency() -> float: def get_child_transparency() -> float:
return _child_transparency return _child_transparency
@@ -241,28 +279,74 @@ def get_child_transparency() -> float:
def set_child_transparency(val: float) -> None: def set_child_transparency(val: float) -> None:
global _child_transparency global _child_transparency
_child_transparency = val _child_transparency = val
apply(_current_palette_name) apply(_current_palette)
def apply(palette_name: str) -> None: def apply(palette_name: str) -> None:
""" """
Apply a named palette by setting all ImGui style colors.
"""
global _current_palette_name Apply a named palette by setting all ImGui style colors and applying global professional styling.
_current_palette_name = palette_name [C: tests/test_theme.py:test_theme_apply_sets_rounding_and_padding]
colours = _PALETTES.get(palette_name, {}) """
if not colours: global _current_palette
# Reset to imgui dark defaults _current_palette = palette_name
imgui.style_colors_dark() if palette_name == 'NERV':
src.theme_nerv.apply_nerv()
return return
# 1. Apply base colors
if palette_name in _PALETTES:
colours = _PALETTES[palette_name]
imgui.style_colors_dark()
style = imgui.get_style()
for col_enum, rgba in colours.items():
style.set_color_(col_enum, imgui.ImVec4(*rgba))
elif hasattr(hello_imgui.ImGuiTheme_, palette_name):
theme_enum = getattr(hello_imgui.ImGuiTheme_, palette_name)
hello_imgui.apply_theme(theme_enum)
else:
# Fallback to Nord Dark if requested but not found, otherwise ImGui Dark
if palette_name == "Nord Dark":
# This should not happen since it's in _PALETTES, but for safety
imgui.style_colors_dark()
else:
imgui.style_colors_dark()
# 2. Apply our "Subtle Rounding" professional tweaks on top of ANY theme
style = imgui.get_style() style = imgui.get_style()
# Start from dark defaults so unlisted keys have sensible values style.window_rounding = 6.0
imgui.style_colors_dark() style.child_rounding = 4.0
for col_enum, rgba in colours.items(): style.frame_rounding = 4.0
col = imgui.ImVec4(*rgba) style.popup_rounding = 4.0
# Apply global transparency overrides style.scrollbar_rounding = 12.0
if col_enum == imgui.Col_.window_bg: col.w *= _transparency style.grab_rounding = 4.0
if col_enum == imgui.Col_.child_bg: col.w *= _child_transparency style.tab_rounding = 4.0
style.set_color_(col_enum, col) style.window_border_size = 1.0
style.frame_border_size = 1.0
style.popup_border_size = 1.0
# Apply transparency to WindowBg
win_bg = style.color_(imgui.Col_.window_bg)
win_bg.w = _transparency
style.set_color_(imgui.Col_.window_bg, win_bg)
# Apply child/frame transparency
for col_idx in [imgui.Col_.child_bg, imgui.Col_.frame_bg, imgui.Col_.popup_bg]:
c = style.color_(col_idx)
c.w = _child_transparency
style.set_color_(col_idx, c)
# Spacing & Padding
style.window_padding = imgui.ImVec2(8.0, 8.0)
style.frame_padding = imgui.ImVec2(8.0, 4.0)
style.item_spacing = imgui.ImVec2(8.0, 4.0)
style.item_inner_spacing = imgui.ImVec2(4.0, 4.0)
style.scrollbar_size = 14.0
# Rendering anti-aliasing (Shaders/Quality)
style.anti_aliased_lines = True
style.anti_aliased_fill = True
style.anti_aliased_lines_use_tex = True
def set_scale(factor: float) -> None: def set_scale(factor: float) -> None:
"""Set the global font/UI scale factor.""" """Set the global font/UI scale factor."""
@@ -272,29 +356,40 @@ def set_scale(factor: float) -> None:
style.font_scale_main = factor style.font_scale_main = factor
def save_to_config(config: dict) -> None: def save_to_config(config: dict) -> None:
"""Persist theme settings into the config dict.""" """Persist theme settings into the config dict under [theme]."""
import sys
config.setdefault("theme", {}) config.setdefault("theme", {})
config["theme"]["palette"] = _current_palette_name config["theme"]["palette"] = _current_palette
config["theme"]["font_path"] = _current_font_path config["theme"]["font_path"] = _current_font_path
config["theme"]["font_size"] = _current_font_size config["theme"]["font_size"] = _current_font_size
config["theme"]["scale"] = _current_scale config["theme"]["scale"] = _current_scale
config["theme"]["transparency"] = _transparency config["theme"]["transparency"] = _transparency
config["theme"]["child_transparency"] = _child_transparency config["theme"]["child_transparency"] = _child_transparency
sys.stderr.write(f"[DEBUG theme_2] save_to_config: palette={_current_palette}, transparency={_transparency}\n")
sys.stderr.flush()
def load_from_config(config: dict) -> None: def load_from_config(config: dict) -> None:
"""Read theme settings from config.""" """Read [theme] from config. Font is handled separately at startup."""
global _current_palette_name, _current_font_path, _current_font_size, _current_scale, _transparency, _child_transparency import sys
global _current_font_path, _current_font_size, _current_scale, _current_palette, _transparency, _child_transparency
t = config.get("theme", {}) t = config.get("theme", {})
_current_palette_name = t.get("palette", "10x Dark") sys.stderr.write(f"[DEBUG theme_2] load_from_config raw: {t}\n")
_current_font_path = t.get("font_path", "") sys.stderr.flush()
_current_palette = t.get("palette", "10x Dark")
if _current_palette in ("", "DPG Default"):
_current_palette = "10x Dark"
_current_font_path = t.get("font_path", "fonts/Inter-Regular.ttf")
_current_font_size = float(t.get("font_size", 16.0)) _current_font_size = float(t.get("font_size", 16.0))
_current_scale = float(t.get("scale", 1.0)) _current_scale = float(t.get("scale", 1.0))
_transparency = float(t.get("transparency", 1.0)) _transparency = float(t.get("transparency", 1.0))
_child_transparency = float(t.get("child_transparency", 1.0)) _child_transparency = float(t.get("child_transparency", 1.0))
sys.stderr.write(f"[DEBUG theme_2] load_from_config effective: palette={_current_palette}, transparency={_transparency}\n")
sys.stderr.flush()
def apply_current() -> None: def apply_current() -> None:
"""Apply the loaded palette and scale. Call after imgui context exists.""" """Apply the loaded palette and scale. Call after imgui context exists."""
apply(_current_palette_name) apply(_current_palette)
set_scale(_current_scale) set_scale(_current_scale)
def get_font_loading_params() -> tuple[str, float]: def get_font_loading_params() -> tuple[str, float]:
@@ -304,14 +399,20 @@ def get_font_loading_params() -> tuple[str, float]:
def get_tweaked_theme() -> hello_imgui.ImGuiTweakedTheme: def get_tweaked_theme() -> hello_imgui.ImGuiTweakedTheme:
"""Returns an ImGuiTweakedTheme object reflecting the current state.""" """Returns an ImGuiTweakedTheme object reflecting the current state."""
tt = hello_imgui.ImGuiTweakedTheme() tt = hello_imgui.ImGuiTweakedTheme()
# Since custom palettes like '10x Dark' are not in hello_imgui enum, if hasattr(hello_imgui.ImGuiTheme_, _current_palette):
# we always use dark as base and apply our specific colors in apply() tt.theme = getattr(hello_imgui.ImGuiTheme_, _current_palette)
tt.theme = hello_imgui.ImGuiTheme_.imgui_colors_dark else:
tt.theme = hello_imgui.ImGuiTheme_.imgui_colors_dark
# Sync tweaks
tt.tweaks.rounding = 6.0 tt.tweaks.rounding = 6.0
return tt return tt
def ai_text_color() -> imgui.ImVec4: def ai_text_color() -> imgui.ImVec4:
return imgui.ImVec4(0.8, 0.9, 0.8, 1.0) """Returns DATA_GREEN if NERV is active, otherwise standard text color."""
if is_nerv_active():
return imgui.ImVec4(*DATA_GREEN)
return imgui.get_style().color_(imgui.Col_.text)
def ai_text_style(): def ai_text_style():
"""Context manager for AI response text styling.""" """Context manager for AI response text styling."""
@@ -319,22 +420,15 @@ def ai_text_style():
def get_role_tint(role: str) -> imgui.ImVec4: def get_role_tint(role: str) -> imgui.ImVec4:
"""Returns a subtle background tint color based on the message role.""" """Returns a subtle background tint color based on the message role."""
# Slightly more opaque and distinct tints for role-based structure # Deep, low-saturation tints for distinct role hierarchy
if role == "User": return imgui.ImVec4(0.12, 0.18, 0.30, 0.6) # Deep Blue if role == "User": return imgui.ImVec4(0.12, 0.18, 0.30, 0.6) # Midnight Blue
elif role == "AI": return imgui.ImVec4(0.14, 0.25, 0.18, 0.6) # Deep Green elif role == "AI": return imgui.ImVec4(0.14, 0.25, 0.18, 0.6) # Forest Green
elif role == "Vendor API": return imgui.ImVec4(0.25, 0.22, 0.12, 0.5) # Earthy Gold elif role == "Vendor API": return imgui.ImVec4(0.25, 0.22, 0.12, 0.5) # Bronze
return imgui.ImVec4(0.1, 0.1, 0.1, 0.4) # Dim System return imgui.ImVec4(0.1, 0.1, 0.1, 0.4) # Neutral System
def is_nerv_active() -> bool:
return _current_palette_name == "Nerv"
from src.theme_nerv_fx import AlertPulsing, CRTFilter
_alert_pulsing = AlertPulsing()
_crt_filter = CRTFilter()
def render_post_fx(width: float, height: float, ai_status: str, crt_enabled: bool) -> None: def render_post_fx(width: float, height: float, ai_status: str, crt_enabled: bool) -> None:
"""Updates and renders the alert and CRT filters.""" """Updates and renders the alert and CRT filters."""
_alert_pulsing.update(ai_status) _alert_pulsing.update(ai_status)
_alert_pulsing.render(width, height) _alert_pulsing.render(width, height)
_crt_filter.enabled = crt_enabled _crt_filter.enabled = crt_enabled
_crt_filter.render(width, height) _crt_filter.render(width, height)