diff --git a/conductor/tracks.md b/conductor/tracks.md index 3e36c6b..6496203 100644 --- a/conductor/tracks.md +++ b/conductor/tracks.md @@ -48,7 +48,7 @@ This file tracks all major tracks for the project. Each track has its own detail *Link: [./tracks/markdown_highlighting_20260308/](./tracks/markdown_highlighting_20260308/)* *Goal: Add rich text rendering with GFM support and syntax highlighting for PowerShell, Python, and JSON/TOML in read-only message and log views.* -5. [x] **Track: NERV UI Theme Integration** +5. [~] **Track: NERV UI Theme Integration** *Link: [./tracks/nerv_ui_theme_20260309/](./tracks/nerv_ui_theme_20260309/)* *Goal: Implement a NERV UI theme for ImGui/Dear PyGui, inspired by technical/military consoles, with CRT effects and a black-void aesthetic.* diff --git a/conductor/tracks/nerv_ui_theme_20260309/index.md b/conductor/tracks/nerv_ui_theme_20260309/index.md new file mode 100644 index 0000000..11cd53e --- /dev/null +++ b/conductor/tracks/nerv_ui_theme_20260309/index.md @@ -0,0 +1,5 @@ +# Track nerv_ui_theme_20260309 Context + +- [Specification](./spec.md) +- [Implementation Plan](./plan.md) +- [Metadata](./metadata.json) diff --git a/conductor/tracks/nerv_ui_theme_20260309/metadata.json b/conductor/tracks/nerv_ui_theme_20260309/metadata.json new file mode 100644 index 0000000..f25544b --- /dev/null +++ b/conductor/tracks/nerv_ui_theme_20260309/metadata.json @@ -0,0 +1,8 @@ +{ + "description": "Implement a NERV UI theme for ImGui/Dear PyGui, inspired by technical/military consoles, with CRT effects and a black-void aesthetic.", + "track_id": "nerv_ui_theme_20260309", + "type": "feature", + "created_at": "2026-03-09T00:35:48Z", + "status": "new", + "updated_at": "2026-03-09T00:35:48Z" +} diff --git a/conductor/tracks/nerv_ui_theme_20260309/plan.md b/conductor/tracks/nerv_ui_theme_20260309/plan.md index 0843e97..1b41d69 100644 --- a/conductor/tracks/nerv_ui_theme_20260309/plan.md +++ b/conductor/tracks/nerv_ui_theme_20260309/plan.md @@ -11,21 +11,33 @@ - [x] Task: Write unit tests to verify that the NERV theme correctly applies colors and geometry settings. de0d9f3 - [x] Task: Conductor - User Manual Verification 'Phase 2: Base NERV Theme Implementation' (Protocol in workflow.md) 9c38ea7 -## Phase 3: Visual Effects (Scanlines & Status Flickering) [checkpoint: 4f4fa10] +## Phase 3: Visual Effects (Scanlines & Status Flickering) [checkpoint: ceb0c7d] - [x] Task: Research how to implement a scanline overlay in ImGui (e.g., using a full-screen transparent texture or a custom draw list). 05a2b8e - [x] Task: Implement the subtle scanline overlay (6% opacity). 05a2b8e - [x] Task: Implement "Status Flickering" logic for active system indicators (e.g., a periodic alpha modification for specific text elements). 05a2b8e - [x] Task: Write tests to verify the visual effect triggers (e.g., checking if the scanline overlay is rendered). 4f4fa10 -- [x] Task: Conductor - User Manual Verification 'Phase 3: Visual Effects' (Protocol in workflow.md) 4f4fa10 +- [x] Task: Conductor - User Manual Verification 'Phase 3: Visual Effects' (Protocol in workflow.md) ceb0c7d -## Phase 4: Alert Pulsing & Error States -- [ ] Task: Implement "Alert Pulsing" logic that can be triggered by application error events. -- [ ] Task: Integrate Alert Pulsing with the NERV theme (shifting borders/background to Alert Red). -- [ ] Task: Write tests to verify that an error state triggers the pulsing effect in the NERV theme. -- [ ] Task: Conductor - User Manual Verification 'Phase 4: Alert Pulsing & Error States' (Protocol in workflow.md) +## Phase 4: Alert Pulsing & Error States [checkpoint: d9495f6] +- [x] Task: Implement "Alert Pulsing" logic that can be triggered by application error events. d9495f6 +- [x] Task: Integrate Alert Pulsing with the NERV theme (shifting borders/background to Alert Red). d9495f6 +- [x] Task: Write tests to verify that an error state triggers the pulsing effect in the NERV theme. d9495f6 +- [x] Task: Conductor - User Manual Verification 'Phase 4: Alert Pulsing & Error States' (Protocol in workflow.md) d9495f6 -## Phase 5: Integration & Theme Selector -- [ ] Task: Add "NERV" to the theme selection dropdown in src/gui_2.py. -- [ ] Task: Ensure that switching to the NERV theme correctly initializes all visual effects (scanlines, etc.). -- [ ] Task: Final UX verification and performance check of the NERV theme. -- [ ] Task: Conductor - User Manual Verification 'Phase 5: Integration & Theme Selector' (Protocol in workflow.md) +## Phase 5: Integration & Theme Selector [checkpoint: afcb1bf] +- [x] Task: Add "NERV" to the theme selection dropdown in src/gui_2.py. afcb1bf +- [x] Task: Ensure that switching to the NERV theme correctly initializes all visual effects (scanlines, etc.). afcb1bf +- [x] Task: Final UX verification and performance check of the NERV theme. afcb1bf +- [x] Task: Conductor - User Manual Verification 'Phase 5: Integration & Theme Selector' (Protocol in workflow.md) afcb1bf + +## Phase 6: NERV Theme Refinement (Contrast & Readability) +- [ ] Task: Fix text readability by ensuring high-contrast text on bright backgrounds (e.g., black text on orange title bars). +- [ ] Task: Adjust the NERV palette to use Data Green or Steel for standard text, reserving Orange for accents and backgrounds. +- [ ] Task: Update gui_2.py to push/pop style colors for headers if necessary to maintain readability. +- [ ] Task: Conductor - User Manual Verification 'Phase 6: NERV Theme Refinement' (Protocol in workflow.md) + +## Phase 7: CRT Filter Implementation +- [ ] Task: Research and implement a more sophisticated "CRT Filter" beyond simple scanlines (e.g., adding a vignette, noise, or subtle color aberration). +- [ ] Task: Implement a "CRT Filter" toggle in the theme settings. +- [ ] Task: Integrate the new CRT filter into the gui_2.py rendering loop. +- [ ] Task: Conductor - User Manual Verification 'Phase 7: CRT Filter Implementation' (Protocol in workflow.md) diff --git a/conductor/tracks/nerv_ui_theme_20260309/spec.md b/conductor/tracks/nerv_ui_theme_20260309/spec.md new file mode 100644 index 0000000..0ab0911 --- /dev/null +++ b/conductor/tracks/nerv_ui_theme_20260309/spec.md @@ -0,0 +1,37 @@ +# Specification: NERV UI Theme Integration + +## Overview +This track aims to implement a new "NERV" visual theme for the manual_slop application, inspired by the aesthetic of technical/military consoles (e.g., Evangelion's NERV UI). The theme will be added as a selectable option within the application, allowing users to switch between the existing theme and the new NERV style without altering the core user experience or layout. + +## Functional Requirements +- **Theme Selection:** Integrate a "NERV" theme option into the existing UI (e.g., in the configuration or theme settings). +- **Color Palette:** Implement the "Black Void" aesthetic using absolute black (#000000) for the background and CRT-inspired phosphor colors: + - **NERV Orange (#FF9830):** Primary accents, headers, active borders. + - **Data Green (#50FF50):** Terminal output, "Nominal" status, standard data. + - **Wire Cyan (#20F0FF):** Structural separators, inactive borders. + - **Alert Red (#FF4840):** Error states, critical alerts. + - **Steel (#E0E0D8):** Secondary text, timestamps. +- **Hard Edges:** Configure all UI elements (windows, frames, buttons) to have zero rounded corners (Rounding = 0.0). +- **Typography:** Utilize a monospace font (e.g., IBM Plex Mono or the project's current monospace font) for all text to maintain a technical look. +- **Visual Effects:** + - **Scanline Overlay:** Implement a subtle CRT-style scanline overlay (approx. 6% opacity). + - **Status Flickering:** Add subtle flickering effects to active system status indicators. + - **Alert Pulsing:** Implement red background or border pulsing during error or critical system states. + +## Non-Functional Requirements +- **Performance:** Ensure the scanline overlay and status flickering do not significantly degrade UI responsiveness or increase CPU usage. +- **Maintainability:** The theme should be implemented in a way that is consistent with the existing theme.py or theme_2.py architecture. + +## Acceptance Criteria +- [ ] Users can select "NERV" from the theme selector. +- [ ] The background is solid black (#000000). +- [ ] All borders and buttons have zero rounded corners. +- [ ] The NERV color palette is correctly applied to all UI elements. +- [ ] The scanline overlay is visible and subtle. +- [ ] Active status indicators exhibit the "Status Flickering" effect. +- [ ] Errors trigger the "Alert Pulsing" effect. + +## Out of Scope +- **Bilingual Labels:** Japanese sub-labels will not be implemented. +- **Layout Changes:** No radical changes to window positioning or spacing. +- **New Features:** This track is purely visual and does not add new application functionality. diff --git a/src/gui_2.py b/src/gui_2.py index 6555908..c7b47b3 100644 --- a/src/gui_2.py +++ b/src/gui_2.py @@ -222,6 +222,9 @@ class App: is_md = label in ("message", "text", "content") ctx_id = f"heavy_{label}_{id_suffix}" + is_nerv = theme.is_nerv_active() + if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80)) + if len(content) > COMMS_CLAMP_CHARS: imgui.begin_child(f"heavy_text_child_{label}_{id_suffix}", imgui.ImVec2(0, 80), True) if is_md: @@ -234,6 +237,8 @@ class App: markdown_helper.render(content, context_id=ctx_id) else: markdown_helper.render_code(content, context_id=ctx_id) + + if is_nerv: imgui.pop_style_color() # ---------------------------------------------------------------- gui @@ -1367,7 +1372,10 @@ def hello(): if is_thinking: val = math.sin(time.time() * 10 * math.pi) alpha = 1.0 if val > 0 else 0.0 - imgui.text_colored(imgui.ImVec4(1.0, 0.39, 0.39, alpha), "THINKING...") + c = vec4(255, 100, 100, alpha) + if theme.is_nerv_active(): + c = vec4(255, 50, 50, alpha) # More vibrant for NERV + imgui.text_colored(c, "THINKING...") imgui.separator() # Prior session viewing mode if self.is_viewing_prior_session: @@ -1405,7 +1413,10 @@ def hello(): if len(content) > 80: preview += "..." imgui.text_colored(vec4(180, 180, 180), preview) else: + is_nerv = theme.is_nerv_active() + if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80)) markdown_helper.render(content, context_id=f'prior_disc_{idx}') + if is_nerv: imgui.pop_style_color() imgui.separator() imgui.pop_id() @@ -1563,15 +1574,21 @@ def hello(): content = entry["content"] pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?") matches = list(pattern.finditer(content)) + is_nerv = theme.is_nerv_active() if not matches: + if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80)) markdown_helper.render(content, context_id=f'disc_{i}') + if is_nerv: imgui.pop_style_color() else: imgui.begin_child(f"read_content_{i}", imgui.ImVec2(0, 150), True) if self.ui_word_wrap: imgui.push_text_wrap_pos(imgui.get_content_region_avail().x) last_idx = 0 for m_idx, match in enumerate(matches): before = content[last_idx:match.start()] - if before: markdown_helper.render(before, context_id=f'disc_{i}_b_{m_idx}') + if before: + if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80)) + markdown_helper.render(before, context_id=f'disc_{i}_b_{m_idx}') + if is_nerv: imgui.pop_style_color() header_text = match.group(0).split("\n")[0].strip() path = match.group(2) code_block = match.group(4) @@ -1584,10 +1601,15 @@ def hello(): self.show_text_viewer = True if code_block: # Render code block with highlighting + if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80)) markdown_helper.render(code_block, context_id=f'disc_{i}_c_{m_idx}') + if is_nerv: imgui.pop_style_color() last_idx = match.end() after = content[last_idx:] - if after: markdown_helper.render(after, context_id=f'disc_{i}_a') + if after: + if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80)) + markdown_helper.render(after, context_id=f'disc_{i}_a') + if is_nerv: imgui.pop_style_color() if self.ui_word_wrap: imgui.pop_text_wrap_pos() imgui.end_child() else: @@ -1850,7 +1872,10 @@ def hello(): if is_live: val = math.sin(time.time() * 10 * math.pi) alpha = 1.0 if val > 0 else 0.0 - imgui.text_colored(imgui.ImVec4(0.39, 1.0, 0.39, alpha), "LIVE") + c = imgui.ImVec4(0.39, 1.0, 0.39, alpha) + if theme.is_nerv_active(): + c = vec4(80, 255, 80, alpha) # DATA_GREEN for LIVE in NERV + imgui.text_colored(c, "LIVE") imgui.separator() ch, self.ui_ai_input = imgui.input_text_multiline("##ai_in", self.ui_ai_input, imgui.ImVec2(-1, -40)) # Keyboard shortcuts @@ -1905,7 +1930,10 @@ def hello(): # --- Always Render Content --- imgui.begin_child("response_scroll_area", imgui.ImVec2(0, -40), True) + is_nerv = theme.is_nerv_active() + if is_nerv: imgui.push_style_color(imgui.Col_.text, vec4(80, 255, 80)) markdown_helper.render(self.ai_response, context_id="response") + if is_nerv: imgui.pop_style_color() imgui.end_child() imgui.separator() @@ -1918,7 +1946,10 @@ def hello(): def _render_comms_history_panel(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_comms_history_panel") - imgui.text_colored(vec4(200, 220, 160), f"Status: {self.ai_status}") + st_col = vec4(200, 220, 160) + if theme.is_nerv_active(): + st_col = vec4(80, 255, 80) # DATA_GREEN for status in NERV + imgui.text_colored(st_col, f"Status: {self.ai_status}") imgui.same_line() if imgui.button("Clear##comms"): ai_client.clear_comms_log() @@ -2300,8 +2331,11 @@ def hello(): def _render_mma_dashboard(self) -> None: if self.perf_profiling_enabled: self.perf_monitor.start_component("_render_mma_dashboard") + is_nerv = theme.is_nerv_active() if self.is_viewing_prior_session: - imgui.text_colored(vec4(255, 200, 100), "HISTORICAL VIEW - READ ONLY") + c = vec4(255, 200, 100) + if is_nerv: c = vec4(255, 152, 48) # NERV_ORANGE + imgui.text_colored(c, "HISTORICAL VIEW - READ ONLY") if self.perf_profiling_enabled: self.perf_monitor.end_component("_render_mma_dashboard") return # Task 5.3: Dense Summary Line @@ -2324,7 +2358,9 @@ def hello(): imgui.text(" | Status:") imgui.same_line() if self.mma_status == "paused": - imgui.text_colored(imgui.ImVec4(1, 0.5, 0, 1), "PIPELINE PAUSED") + c = imgui.ImVec4(1, 0.5, 0, 1) + if is_nerv: c = vec4(255, 152, 48) + imgui.text_colored(c, "PIPELINE PAUSED") imgui.same_line() status_col = imgui.ImVec4(1, 1, 1, 1) if self.mma_status == "idle": status_col = imgui.ImVec4(0.7, 0.7, 0.7, 1) @@ -2332,6 +2368,11 @@ def hello(): elif self.mma_status == "done": status_col = imgui.ImVec4(0, 1, 0, 1) elif self.mma_status == "error": status_col = imgui.ImVec4(1, 0, 0, 1) elif self.mma_status == "paused": status_col = imgui.ImVec4(1, 0.5, 0, 1) + + if is_nerv: + if self.mma_status == "running": status_col = vec4(80, 255, 80) # DATA_GREEN + elif self.mma_status == "error": status_col = vec4(255, 72, 64) # ALERT_RED + imgui.text_colored(status_col, self.mma_status.upper()) imgui.same_line() imgui.text(" | Cost:") @@ -2412,7 +2453,9 @@ def hello(): if status == "new": imgui.text_colored(imgui.ImVec4(0.7, 0.7, 0.7, 1.0), "NEW") elif status == "active": - imgui.text_colored(imgui.ImVec4(1.0, 1.0, 0.0, 1.0), "ACTIVE") + c = imgui.ImVec4(1.0, 1.0, 0.0, 1.0) + if is_nerv: c = vec4(80, 255, 80) + imgui.text_colored(c, "ACTIVE") elif status == "done": imgui.text_colored(imgui.ImVec4(0.0, 1.0, 0.0, 1.0), "DONE") elif status == "blocked": @@ -2478,7 +2521,9 @@ def hello(): if any_pending: alpha = abs(math.sin(time.time() * 5)) imgui.same_line() - imgui.text_colored(imgui.ImVec4(1.0, 0.3, 0.3, alpha), " APPROVAL PENDING") + c = imgui.ImVec4(1.0, 0.3, 0.3, alpha) + if is_nerv: c = vec4(255, 72, 64, alpha) # ALERT_RED + imgui.text_colored(c, " APPROVAL PENDING") imgui.same_line() if imgui.button("Go to Approval"): pass # scroll/focus handled by existing dialog rendering diff --git a/src/theme_nerv.py b/src/theme_nerv.py index 5be268b..53f9e1c 100644 --- a/src/theme_nerv.py +++ b/src/theme_nerv.py @@ -12,7 +12,7 @@ STEEL = _c(224, 224, 216) BLACK = _c(0, 0, 0) NERV_PALETTE = { - imgui.Col_.text: NERV_ORANGE, + imgui.Col_.text: STEEL, imgui.Col_.window_bg: BLACK, imgui.Col_.child_bg: BLACK, imgui.Col_.popup_bg: BLACK, @@ -22,7 +22,7 @@ NERV_PALETTE = { imgui.Col_.frame_bg_hovered: _c(255, 152, 48, 40), imgui.Col_.frame_bg_active: _c(255, 152, 48, 80), imgui.Col_.title_bg: BLACK, - imgui.Col_.title_bg_active: NERV_ORANGE, + imgui.Col_.title_bg_active: BLACK, imgui.Col_.title_bg_collapsed: BLACK, imgui.Col_.menu_bar_bg: BLACK, imgui.Col_.scrollbar_bg: BLACK, @@ -33,11 +33,11 @@ NERV_PALETTE = { imgui.Col_.slider_grab: WIRE_CYAN, imgui.Col_.slider_grab_active: DATA_GREEN, imgui.Col_.button: BLACK, - imgui.Col_.button_hovered: NERV_ORANGE, - imgui.Col_.button_active: ALERT_RED, - imgui.Col_.header: _c(255, 152, 48, 120), - imgui.Col_.header_hovered: NERV_ORANGE, - imgui.Col_.header_active: ALERT_RED, + imgui.Col_.button_hovered: _c(255, 152, 48, 80), + imgui.Col_.button_active: _c(255, 152, 48, 120), + imgui.Col_.header: _c(255, 152, 48, 60), + imgui.Col_.header_hovered: _c(255, 152, 48, 100), + imgui.Col_.header_active: _c(255, 152, 48, 140), imgui.Col_.separator: STEEL, imgui.Col_.separator_hovered: WIRE_CYAN, imgui.Col_.separator_active: DATA_GREEN, @@ -45,8 +45,8 @@ NERV_PALETTE = { imgui.Col_.resize_grip_hovered: STEEL, imgui.Col_.resize_grip_active: WIRE_CYAN, imgui.Col_.tab: BLACK, - imgui.Col_.tab_hovered: NERV_ORANGE, - imgui.Col_.tab_selected: NERV_ORANGE, + imgui.Col_.tab_hovered: _c(255, 152, 48, 100), + imgui.Col_.tab_selected: _c(255, 152, 48, 120), imgui.Col_.tab_dimmed: BLACK, imgui.Col_.tab_dimmed_selected: _c(255, 152, 48, 80), imgui.Col_.plot_lines: WIRE_CYAN, diff --git a/tests/test_theme_nerv.py b/tests/test_theme_nerv.py index b2e4383..78b94dd 100644 --- a/tests/test_theme_nerv.py +++ b/tests/test_theme_nerv.py @@ -34,7 +34,8 @@ def test_apply_nerv_sets_rounding_and_colors(monkeypatch): # Verify key colors # window_bg should be BLACK (0, 0, 0, 1.0) - # text should be NERV_ORANGE (255/255.0, 152/255.0, 48/255.0, 1.0) + # text should be STEEL (224/255.0, 224/255.0, 216/255.0, 1.0) + # title_bg_active should be BLACK (0, 0, 0, 1.0) # Extract calls to set_color_ # Using real imgui.Col_ values for keys because they are bound in NERV_PALETTE at import time @@ -44,4 +45,7 @@ def test_apply_nerv_sets_rounding_and_colors(monkeypatch): assert color_calls[imgui.Col_.window_bg] == (0.0, 0.0, 0.0, 1.0) assert imgui.Col_.text in color_calls - assert color_calls[imgui.Col_.text] == (1.0, 152/255.0, 48/255.0, 1.0) + assert color_calls[imgui.Col_.text] == (224/255.0, 224/255.0, 216/255.0, 1.0) + + assert imgui.Col_.title_bg_active in color_calls + assert color_calls[imgui.Col_.title_bg_active] == (0.0, 0.0, 0.0, 1.0)