#![allow(
dead_code,
reason = "Bridge module; consumers wire up in Plan C integration"
)]
use oxi_tui::widget::RenderCtx;
use oxi_tui::widget::Renderable;
use oxi_tui_legacy::Symbols;
use oxi_tui_legacy::theme::{ColorScheme, Spacing, Theme as LegacyTheme};
use ratatui::layout::Rect;
use crate::tui::overlay::OverlayComponent;
pub struct LegacyOverlayAdapter {
overlay: Box<dyn OverlayComponent>,
dirty_seq: u64,
}
impl LegacyOverlayAdapter {
pub fn new(overlay: Box<dyn OverlayComponent>) -> Self {
Self {
overlay,
dirty_seq: 1,
}
}
pub fn overlay_mut(&mut self) -> &mut Box<dyn OverlayComponent> {
&mut self.overlay
}
}
impl Renderable for LegacyOverlayAdapter {
fn content_hash(&self) -> u64 {
self.dirty_seq
}
fn height_for(&self, _width: u16, _ctx: &RenderCtx) -> u16 {
0
}
fn render(&mut self, area: Rect, ctx: &mut RenderCtx) {
let legacy_theme = convert_theme(ctx.theme());
ctx.with_frame(|frame| {
self.overlay.render(frame, area, &legacy_theme);
});
self.dirty_seq = self.dirty_seq.wrapping_add(1).max(1);
}
}
#[must_use]
pub fn convert_theme(v2: &oxi_tui::theme::Theme) -> LegacyTheme {
let v2c = &v2.colors;
let legacy_colors = ColorScheme {
foreground: v2c.foreground,
background: v2c.background,
primary: v2c.primary,
secondary: v2c.muted,
error: v2c.error,
warning: v2c.warning,
success: v2c.success,
muted: v2c.muted,
accent: v2c.accent,
border: v2c.border,
user_border: v2c.primary,
user_bg: v2c.user_bg,
cursor_fg: v2c.background,
cursor_bg: v2c.foreground,
selection_bg: v2c.selection_bg,
code_fg: v2c.foreground,
code_bg: v2c.code_bg,
tool_pending_bg: v2c.surface_bg,
tool_executing_bg: v2c.surface_bg,
tool_success_bg: v2c.success,
tool_error_bg: v2c.error,
response_bg: v2c.response_bg,
thinking_bg: v2c.thinking_bg,
surface_bg: v2c.surface_bg,
panel_bg: v2c.panel_bg,
diff_add_bg: v2c.diff_add_bg,
diff_remove_bg: v2c.diff_remove_bg,
diff_hunk_bg: v2c.diff_hunk_bg,
};
LegacyTheme {
name: v2.name.to_string(),
colors: legacy_colors,
spacing: Spacing::default(),
symbols: Symbols::default(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyEvent;
struct NotSendOverlay(*mut ());
impl std::fmt::Debug for NotSendOverlay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_tuple("NotSendOverlay").finish()
}
}
impl OverlayComponent for NotSendOverlay {
fn handle_key(&mut self, _key: KeyEvent) -> crate::tui::overlay::OverlayAction {
crate::tui::overlay::OverlayAction::None
}
fn render(&mut self, _frame: &mut ratatui::Frame, _area: Rect, _theme: &LegacyTheme) {}
fn hint(&self) -> &str {
""
}
}
struct PaintingOverlay {
sentinel: &'static str,
_not_send_witness: *mut (),
}
impl std::fmt::Debug for PaintingOverlay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PaintingOverlay")
.field("sentinel", &self.sentinel)
.finish()
}
}
impl OverlayComponent for PaintingOverlay {
fn handle_key(&mut self, _key: KeyEvent) -> crate::tui::overlay::OverlayAction {
crate::tui::overlay::OverlayAction::None
}
fn render(&mut self, frame: &mut ratatui::Frame, area: Rect, _theme: &LegacyTheme) {
frame.buffer_mut()[(area.x, area.y)].set_symbol(self.sentinel);
}
fn hint(&self) -> &str {
""
}
}
#[test]
fn adapter_accepts_non_send_overlay() {
let overlay: Box<dyn OverlayComponent> = Box::new(NotSendOverlay(std::ptr::null_mut()));
let mut adapter = LegacyOverlayAdapter::new(overlay);
let _inner = adapter.overlay_mut();
}
#[test]
fn content_hash_changes_after_render() {
let overlay: Box<dyn OverlayComponent> = Box::new(NotSendOverlay(std::ptr::null_mut()));
let mut adapter = LegacyOverlayAdapter::new(overlay);
assert_eq!(adapter.content_hash(), 1);
assert_eq!(adapter.content_hash(), 1);
adapter.dirty_seq = adapter.dirty_seq.wrapping_add(1).max(1);
assert_ne!(adapter.content_hash(), 1);
}
#[test]
fn adapter_wraps_real_overlay() {
use crate::tui::overlay::mcp_config::McpConfigOverlay;
use std::path::PathBuf;
let real: Box<dyn OverlayComponent> =
Box::new(McpConfigOverlay::new(None, PathBuf::from(".")));
let mut adapter = LegacyOverlayAdapter::new(real);
let h0 = adapter.content_hash();
assert!(h0 != 0);
adapter.dirty_seq = adapter.dirty_seq.wrapping_add(1).max(1);
let h1 = adapter.content_hash();
assert_ne!(h0, h1, "dirty_seq must change after render");
}
#[test]
fn convert_theme_preserves_core_slots() {
use oxi_tui::theme::Theme;
let v2 = Theme::dark();
let legacy = convert_theme(&v2);
assert_eq!(legacy.name, "dark");
assert_eq!(legacy.colors.foreground, v2.colors.foreground);
assert_eq!(legacy.colors.background, v2.colors.background);
assert_eq!(legacy.colors.primary, v2.colors.primary);
assert_eq!(legacy.colors.border, v2.colors.border);
assert_eq!(legacy.spacing.padding, 1);
}
#[test]
fn render_delegates_to_legacy_overlay() {
use oxi_tui::theme::{TerminalCaps, Theme};
use oxi_tui::widget::RenderCtx;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let backend = TestBackend::new(10, 5);
let mut term = Terminal::new(backend).unwrap();
let theme = Theme::dark();
let caps = TerminalCaps::default();
let overlay: Box<dyn OverlayComponent> = Box::new(PaintingOverlay {
sentinel: "X",
_not_send_witness: std::ptr::null_mut(),
});
let mut adapter = LegacyOverlayAdapter::new(overlay);
let h_before = adapter.content_hash();
term.draw(|f| {
let mut ctx = RenderCtx::new(f, &theme, &caps);
let area = Rect {
x: 0,
y: 0,
width: 10,
height: 5,
};
adapter.render(area, &mut ctx);
})
.unwrap();
let buf = term.backend().buffer().clone();
let symbol = buf.cell((0, 0)).map_or("", ratatui::buffer::Cell::symbol);
assert_eq!(
symbol, "X",
"legacy overlay's sentinel cell must be visible after adapter.render"
);
assert_ne!(
adapter.content_hash(),
h_before,
"render() must bump dirty_seq so the next content_hash differs"
);
}
}