1mod commands;
20mod settings_menu;
21
22use std::{any::TypeId, sync::Arc};
23
24pub use commands::{
26 SettingsMenuClose, SettingsMenuCycleNext, SettingsMenuCyclePrev, SettingsMenuDecrement,
27 SettingsMenuExecuteAction, SettingsMenuIncrement, SettingsMenuOpen, SettingsMenuQuick1,
28 SettingsMenuQuick2, SettingsMenuQuick3, SettingsMenuQuick4, SettingsMenuQuick5,
29 SettingsMenuQuick6, SettingsMenuQuick7, SettingsMenuQuick8, SettingsMenuQuick9,
30 SettingsMenuQuickSelect, SettingsMenuSelectNext, SettingsMenuSelectPrev, SettingsMenuToggle,
31};
32
33use reovim_core::{
34 bind::{CommandRef, EditModeKind, KeymapScope},
35 command::id::CommandId,
36 config::ProfileConfig,
37 display::DisplayInfo,
38 event_bus::{EventBus, EventResult, core_events::RequestFocusChange},
39 frame::FrameBuffer,
40 highlight::Theme,
41 keys,
42 modd::ComponentId,
43 option::{ChangeSource, OptionChanged, RegisterOption, RegisterSettingSection},
44 plugin::{
45 EditorContext, Plugin, PluginContext, PluginId, PluginStateRegistry, PluginWindow, Rect,
46 WindowConfig,
47 },
48 screen::border::{BorderConfig, BorderStyle, WindowAdjacency, render_border_to_buffer},
49};
50
51pub use settings_menu::{
53 ActionType, FlatItem, MenuLayout, MessageKind, RegisteredOption, SectionMeta, SettingChange,
54 SettingItem, SettingSection, SettingValue, SettingsInputMode, SettingsMenuState,
55};
56
57pub mod command_id {
59 use super::CommandId;
60
61 pub const SETTINGS_MENU_OPEN: CommandId = CommandId::new("settings_menu_open");
62 pub const SETTINGS_MENU_CLOSE: CommandId = CommandId::new("settings_menu_close");
63 pub const SETTINGS_MENU_NEXT: CommandId = CommandId::new("settings_menu_next");
64 pub const SETTINGS_MENU_PREV: CommandId = CommandId::new("settings_menu_prev");
65 pub const SETTINGS_MENU_TOGGLE: CommandId = CommandId::new("settings_menu_toggle");
66 pub const SETTINGS_MENU_CYCLE_NEXT: CommandId = CommandId::new("settings_menu_cycle_next");
67 pub const SETTINGS_MENU_CYCLE_PREV: CommandId = CommandId::new("settings_menu_cycle_prev");
68 pub const SETTINGS_MENU_INCREMENT: CommandId = CommandId::new("settings_menu_increment");
69 pub const SETTINGS_MENU_DECREMENT: CommandId = CommandId::new("settings_menu_decrement");
70 pub const SETTINGS_MENU_EXECUTE: CommandId = CommandId::new("settings_menu_execute");
71 pub const SETTINGS_MENU_QUICK_1: CommandId = CommandId::new("settings_menu_quick_1");
72 pub const SETTINGS_MENU_QUICK_2: CommandId = CommandId::new("settings_menu_quick_2");
73 pub const SETTINGS_MENU_QUICK_3: CommandId = CommandId::new("settings_menu_quick_3");
74 pub const SETTINGS_MENU_QUICK_4: CommandId = CommandId::new("settings_menu_quick_4");
75 pub const SETTINGS_MENU_QUICK_5: CommandId = CommandId::new("settings_menu_quick_5");
76 pub const SETTINGS_MENU_QUICK_6: CommandId = CommandId::new("settings_menu_quick_6");
77 pub const SETTINGS_MENU_QUICK_7: CommandId = CommandId::new("settings_menu_quick_7");
78 pub const SETTINGS_MENU_QUICK_8: CommandId = CommandId::new("settings_menu_quick_8");
79 pub const SETTINGS_MENU_QUICK_9: CommandId = CommandId::new("settings_menu_quick_9");
80}
81
82pub struct SettingsPluginWindow;
84
85impl PluginWindow for SettingsPluginWindow {
86 fn window_config(
87 &self,
88 state: &Arc<PluginStateRegistry>,
89 _ctx: &EditorContext,
90 ) -> Option<WindowConfig> {
91 state.with::<SettingsMenuState, _, _>(|settings| {
92 if !settings.visible {
93 return None;
94 }
95
96 let layout = &settings.layout;
97 Some(WindowConfig {
98 bounds: Rect::new(layout.x, layout.y, layout.width, layout.height),
99 z_order: 400, visible: true,
101 })
102 })?
103 }
104
105 #[allow(clippy::cast_possible_truncation)]
106 fn render(
107 &self,
108 state: &Arc<PluginStateRegistry>,
109 _ctx: &EditorContext,
110 buffer: &mut FrameBuffer,
111 bounds: Rect,
112 theme: &Theme,
113 ) {
114 let Some(settings) = state.with::<SettingsMenuState, _, _>(Clone::clone) else {
115 tracing::debug!("Settings render: no state available");
116 return;
117 };
118
119 tracing::debug!(
120 "Settings render: visible={}, items={}, selected={}, scroll={}",
121 settings.visible,
122 settings.flat_items.len(),
123 settings.selected_index,
124 settings.scroll_offset
125 );
126
127 if let Some(item) = settings.selected_item() {
129 tracing::debug!(
130 "Settings render: selected item key={}, value={:?}",
131 item.key,
132 item.value
133 );
134 }
135
136 let normal_style = &theme.popup.normal;
137
138 let border_config = BorderConfig::new(BorderStyle::Rounded).with_title("Settings");
140 let adjacency = WindowAdjacency::default();
141 render_border_to_buffer(
142 buffer,
143 bounds.x,
144 bounds.y,
145 bounds.width,
146 bounds.height,
147 &border_config,
148 &theme.popup.border,
149 &adjacency,
150 true, );
152
153 let content_height = bounds.height.saturating_sub(2) as usize;
155 for row in 0..content_height {
156 let y = bounds.y + 1 + row as u16;
157
158 let item_idx = row + settings.scroll_offset;
160 let item = settings.flat_items.get(item_idx);
161 let is_selected = item_idx == settings.selected_index;
162 let style = if is_selected {
163 &theme.popup.selected
164 } else {
165 normal_style
166 };
167
168 let item_text = match item {
169 Some(FlatItem::SectionHeader(name)) => format!("[{name}]"),
170 Some(FlatItem::Setting {
171 section_idx,
172 item_idx,
173 }) => {
174 if let Some(section) = settings.sections.get(*section_idx)
175 && let Some(setting) = section.items.get(*item_idx)
176 {
177 if is_selected {
179 tracing::debug!(
180 "Settings render selected: key={}, value={:?}",
181 setting.key,
182 setting.value
183 );
184 }
185 let value_str = setting.value.display_value();
187 match &setting.value {
188 SettingValue::Bool(b) => {
189 let checkbox = if *b { "[x]" } else { "[ ]" };
190 format!(" {} {}", checkbox, setting.label)
191 }
192 SettingValue::Action(_) => format!(" {}", setting.label),
193 _ => format!(" {}: {}", setting.label, value_str),
194 }
195 } else {
196 " ???".to_string()
197 }
198 }
199 None => String::new(), };
201
202 for (i, ch) in item_text.chars().enumerate() {
204 let x = bounds.x + 1 + i as u16;
205 if x < bounds.x + bounds.width - 1 {
206 buffer.put_char(x, y, ch, style);
207 }
208 }
209
210 for x in (bounds.x + 1 + item_text.len() as u16)..(bounds.x + bounds.width - 1) {
212 buffer.put_char(x, y, ' ', if item.is_some() { style } else { normal_style });
213 }
214 }
215 }
216}
217
218pub const COMPONENT_ID: ComponentId = ComponentId("settings");
220
221pub struct SettingsMenuPlugin;
229
230impl Plugin for SettingsMenuPlugin {
231 fn id(&self) -> PluginId {
232 PluginId::new("reovim:settings-menu")
233 }
234
235 fn name(&self) -> &'static str {
236 "Settings Menu"
237 }
238
239 fn description(&self) -> &'static str {
240 "In-editor settings menu with live preview"
241 }
242
243 fn dependencies(&self) -> Vec<TypeId> {
244 vec![]
245 }
246
247 fn build(&self, ctx: &mut PluginContext) {
248 use reovim_core::highlight::{Color, Style};
250
251 let gray = Color::Rgb {
253 r: 92,
254 g: 99,
255 b: 112,
256 };
257 let fg = Color::Rgb {
258 r: 171,
259 g: 178,
260 b: 191,
261 };
262 let style = Style::new().fg(fg).bg(gray).bold();
263
264 ctx.register_display(COMPONENT_ID, DisplayInfo::new(" SETTINGS ", " ", style));
265
266 let _ = ctx.register_command(SettingsMenuOpen);
268 let _ = ctx.register_command(SettingsMenuClose);
269 let _ = ctx.register_command(SettingsMenuSelectNext);
270 let _ = ctx.register_command(SettingsMenuSelectPrev);
271
272 let _ = ctx.register_command(SettingsMenuToggle);
274 let _ = ctx.register_command(SettingsMenuCycleNext);
275 let _ = ctx.register_command(SettingsMenuCyclePrev);
276 let _ = ctx.register_command(SettingsMenuIncrement);
277 let _ = ctx.register_command(SettingsMenuDecrement);
278 let _ = ctx.register_command(SettingsMenuExecuteAction);
279
280 let _ = ctx.register_command(SettingsMenuQuick1);
282 let _ = ctx.register_command(SettingsMenuQuick2);
283 let _ = ctx.register_command(SettingsMenuQuick3);
284 let _ = ctx.register_command(SettingsMenuQuick4);
285 let _ = ctx.register_command(SettingsMenuQuick5);
286 let _ = ctx.register_command(SettingsMenuQuick6);
287 let _ = ctx.register_command(SettingsMenuQuick7);
288 let _ = ctx.register_command(SettingsMenuQuick8);
289 let _ = ctx.register_command(SettingsMenuQuick9);
290
291 let editor_normal = KeymapScope::editor_normal();
293 let settings_normal = KeymapScope::Component {
294 id: COMPONENT_ID,
295 mode: EditModeKind::Normal,
296 };
297
298 ctx.bind_key_scoped(
300 editor_normal,
301 keys![Space 's'],
302 CommandRef::Registered(command_id::SETTINGS_MENU_OPEN),
303 );
304
305 ctx.bind_key_scoped(
309 settings_normal.clone(),
310 keys!['j'],
311 CommandRef::Registered(command_id::SETTINGS_MENU_NEXT),
312 );
313 ctx.bind_key_scoped(
314 settings_normal.clone(),
315 keys!['k'],
316 CommandRef::Registered(command_id::SETTINGS_MENU_PREV),
317 );
318 ctx.bind_key_scoped(
319 settings_normal.clone(),
320 keys![Down],
321 CommandRef::Registered(command_id::SETTINGS_MENU_NEXT),
322 );
323 ctx.bind_key_scoped(
324 settings_normal.clone(),
325 keys![Up],
326 CommandRef::Registered(command_id::SETTINGS_MENU_PREV),
327 );
328
329 ctx.bind_key_scoped(
331 settings_normal.clone(),
332 keys![Space],
333 CommandRef::Registered(command_id::SETTINGS_MENU_TOGGLE),
334 );
335 ctx.bind_key_scoped(
336 settings_normal.clone(),
337 keys![Enter],
338 CommandRef::Registered(command_id::SETTINGS_MENU_EXECUTE),
339 );
340 ctx.bind_key_scoped(
341 settings_normal.clone(),
342 keys!['l'],
343 CommandRef::Registered(command_id::SETTINGS_MENU_CYCLE_NEXT),
344 );
345 ctx.bind_key_scoped(
346 settings_normal.clone(),
347 keys!['h'],
348 CommandRef::Registered(command_id::SETTINGS_MENU_CYCLE_PREV),
349 );
350 ctx.bind_key_scoped(
351 settings_normal.clone(),
352 keys![Tab],
353 CommandRef::Registered(command_id::SETTINGS_MENU_CYCLE_NEXT),
354 );
355 ctx.bind_key_scoped(
356 settings_normal.clone(),
357 keys![(Shift Tab)],
358 CommandRef::Registered(command_id::SETTINGS_MENU_CYCLE_PREV),
359 );
360
361 ctx.bind_key_scoped(
363 settings_normal.clone(),
364 keys!['+'],
365 CommandRef::Registered(command_id::SETTINGS_MENU_INCREMENT),
366 );
367 ctx.bind_key_scoped(
368 settings_normal.clone(),
369 keys!['-'],
370 CommandRef::Registered(command_id::SETTINGS_MENU_DECREMENT),
371 );
372
373 ctx.bind_key_scoped(
375 settings_normal.clone(),
376 keys!['1'],
377 CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_1),
378 );
379 ctx.bind_key_scoped(
380 settings_normal.clone(),
381 keys!['2'],
382 CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_2),
383 );
384 ctx.bind_key_scoped(
385 settings_normal.clone(),
386 keys!['3'],
387 CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_3),
388 );
389 ctx.bind_key_scoped(
390 settings_normal.clone(),
391 keys!['4'],
392 CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_4),
393 );
394 ctx.bind_key_scoped(
395 settings_normal.clone(),
396 keys!['5'],
397 CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_5),
398 );
399 ctx.bind_key_scoped(
400 settings_normal.clone(),
401 keys!['6'],
402 CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_6),
403 );
404 ctx.bind_key_scoped(
405 settings_normal.clone(),
406 keys!['7'],
407 CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_7),
408 );
409 ctx.bind_key_scoped(
410 settings_normal.clone(),
411 keys!['8'],
412 CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_8),
413 );
414 ctx.bind_key_scoped(
415 settings_normal.clone(),
416 keys!['9'],
417 CommandRef::Registered(command_id::SETTINGS_MENU_QUICK_9),
418 );
419
420 ctx.bind_key_scoped(
422 settings_normal.clone(),
423 keys![Escape],
424 CommandRef::Registered(command_id::SETTINGS_MENU_CLOSE),
425 );
426 ctx.bind_key_scoped(
427 settings_normal,
428 keys!['q'],
429 CommandRef::Registered(command_id::SETTINGS_MENU_CLOSE),
430 );
431 }
432
433 fn init_state(&self, registry: &PluginStateRegistry) {
434 registry.register(SettingsMenuState::new());
436
437 registry.register_plugin_window(Arc::new(SettingsPluginWindow));
439 }
440
441 fn subscribe(&self, bus: &EventBus, state: Arc<PluginStateRegistry>) {
442 {
444 use {
445 crate::SettingsMenuOpen,
446 reovim_core::{
447 command_line::ExCommandHandler,
448 event_bus::{DynEvent, core_events::RegisterExCommand},
449 },
450 };
451
452 bus.emit(RegisterExCommand::new(
453 "settings",
454 ExCommandHandler::ZeroArg {
455 event_constructor: || DynEvent::new(SettingsMenuOpen),
456 description: "Open the settings menu",
457 },
458 ));
459 }
460
461 {
463 let state = Arc::clone(&state);
464 bus.subscribe::<RegisterSettingSection, _>(100, move |event, _ctx| {
465 state.with_mut::<SettingsMenuState, _, _>(|menu| {
466 menu.register_section(
467 event.id.to_string(),
468 SectionMeta {
469 display_name: event.display_name.to_string(),
470 order: event.order,
471 description: event.description.as_ref().map(|s| s.to_string()),
472 },
473 );
474 });
475 EventResult::Handled
476 });
477 }
478
479 {
481 let state = Arc::clone(&state);
482 bus.subscribe::<RegisterOption, _>(100, move |event, _ctx| {
483 state.with_mut::<SettingsMenuState, _, _>(|menu| {
484 menu.register_option(&event.spec, &event.spec.default);
485 });
486 EventResult::Handled
487 });
488 }
489
490 {
493 let state = Arc::clone(&state);
494 bus.subscribe::<OptionChanged, _>(100, move |event, ctx| {
495 if event.source == ChangeSource::SettingsMenu {
497 return EventResult::Handled;
498 }
499
500 state.with_mut::<SettingsMenuState, _, _>(|menu| {
501 menu.update_option_value(&event.name, &event.new_value);
502 });
503 ctx.request_render();
504 EventResult::Handled
505 });
506 }
507
508 {
510 let state = Arc::clone(&state);
511 bus.subscribe::<SettingsMenuOpen, _>(100, move |_event, ctx| {
512 state.with_mut::<SettingsMenuState, _, _>(|menu| {
513 let profile = ProfileConfig::default();
515 menu.open(&profile, "default");
516 menu.calculate_layout(120, 40);
518 });
519 ctx.emit(RequestFocusChange {
521 target: COMPONENT_ID,
522 });
523 ctx.request_render();
524 EventResult::Handled
525 });
526 }
527
528 {
530 let state = Arc::clone(&state);
531 bus.subscribe::<SettingsMenuClose, _>(100, move |_event, ctx| {
532 state.with_mut::<SettingsMenuState, _, _>(|menu| {
533 menu.close();
534 });
535 ctx.emit(RequestFocusChange {
537 target: ComponentId::EDITOR,
538 });
539 ctx.request_render();
540 EventResult::Handled
541 });
542 }
543
544 {
546 let state = Arc::clone(&state);
547 bus.subscribe::<SettingsMenuSelectNext, _>(100, move |_event, ctx| {
548 state.with_mut::<SettingsMenuState, _, _>(|menu| {
549 menu.select_next();
550 });
551 ctx.request_render();
552 EventResult::Handled
553 });
554 }
555
556 {
558 let state = Arc::clone(&state);
559 bus.subscribe::<SettingsMenuSelectPrev, _>(100, move |_event, ctx| {
560 state.with_mut::<SettingsMenuState, _, _>(|menu| {
561 menu.select_prev();
562 });
563 ctx.request_render();
564 EventResult::Handled
565 });
566 }
567
568 {
570 let state = Arc::clone(&state);
571 bus.subscribe::<SettingsMenuToggle, _>(100, move |_event, ctx| {
572 tracing::debug!("SettingsMenuToggle event received");
573 let change = state.with_mut::<SettingsMenuState, _, _>(|menu| {
574 tracing::debug!("Before toggle - selected_index: {}", menu.selected_index);
575 if let Some(item) = menu.selected_item() {
576 tracing::debug!("Selected item: {} = {:?}", item.key, item.value);
577 }
578 let result = menu.toggle_selected();
579 if let Some(item) = menu.selected_item() {
580 tracing::debug!("After toggle: {} = {:?}", item.key, item.value);
581 }
582 result
583 });
584 if let Some(Some(change)) = change {
585 tracing::info!(
586 "Settings toggle: {} changed from {:?} to {:?}",
587 change.key,
588 change.old_value,
589 change.new_value
590 );
591 ctx.emit(OptionChanged::new(
592 change.key,
593 change.old_value,
594 change.new_value,
595 ChangeSource::SettingsMenu,
596 ));
597 } else {
598 tracing::debug!(
599 "Settings toggle: no change (not a boolean or no item selected)"
600 );
601 }
602 ctx.request_render();
603 EventResult::Handled
604 });
605 }
606
607 {
609 let state = Arc::clone(&state);
610 bus.subscribe::<SettingsMenuCycleNext, _>(100, move |_event, ctx| {
611 let change =
612 state.with_mut::<SettingsMenuState, _, _>(|menu| menu.cycle_next_selected());
613 if let Some(Some(change)) = change {
614 tracing::debug!("Settings cycle next: {} changed", change.key);
615 ctx.emit(OptionChanged::new(
616 change.key,
617 change.old_value,
618 change.new_value,
619 ChangeSource::SettingsMenu,
620 ));
621 }
622 ctx.request_render();
623 EventResult::Handled
624 });
625 }
626
627 {
629 let state = Arc::clone(&state);
630 bus.subscribe::<SettingsMenuCyclePrev, _>(100, move |_event, ctx| {
631 let change =
632 state.with_mut::<SettingsMenuState, _, _>(|menu| menu.cycle_prev_selected());
633 if let Some(Some(change)) = change {
634 tracing::debug!("Settings cycle prev: {} changed", change.key);
635 ctx.emit(OptionChanged::new(
636 change.key,
637 change.old_value,
638 change.new_value,
639 ChangeSource::SettingsMenu,
640 ));
641 }
642 ctx.request_render();
643 EventResult::Handled
644 });
645 }
646
647 {
649 let state = Arc::clone(&state);
650 bus.subscribe::<SettingsMenuIncrement, _>(100, move |_event, ctx| {
651 let change =
652 state.with_mut::<SettingsMenuState, _, _>(|menu| menu.increment_selected());
653 if let Some(Some(change)) = change {
654 tracing::debug!("Settings increment: {} changed", change.key);
655 ctx.emit(OptionChanged::new(
656 change.key,
657 change.old_value,
658 change.new_value,
659 ChangeSource::SettingsMenu,
660 ));
661 }
662 ctx.request_render();
663 EventResult::Handled
664 });
665 }
666
667 {
669 let state = Arc::clone(&state);
670 bus.subscribe::<SettingsMenuDecrement, _>(100, move |_event, ctx| {
671 let change =
672 state.with_mut::<SettingsMenuState, _, _>(|menu| menu.decrement_selected());
673 if let Some(Some(change)) = change {
674 tracing::debug!("Settings decrement: {} changed", change.key);
675 ctx.emit(OptionChanged::new(
676 change.key,
677 change.old_value,
678 change.new_value,
679 ChangeSource::SettingsMenu,
680 ));
681 }
682 ctx.request_render();
683 EventResult::Handled
684 });
685 }
686
687 {
689 let state = Arc::clone(&state);
690 bus.subscribe::<SettingsMenuExecuteAction, _>(100, move |_event, ctx| {
691 let action = state
693 .with::<SettingsMenuState, _, _>(|menu| menu.get_selected_action())
694 .flatten();
695
696 if let Some(action_type) = action {
697 match action_type {
699 ActionType::SaveProfile => {
700 tracing::info!("Settings: Save profile action triggered");
701 state.with_mut::<SettingsMenuState, _, _>(|menu| {
703 menu.set_message(
704 "Profile save not yet implemented".to_string(),
705 MessageKind::Info,
706 );
707 });
708 }
709 ActionType::LoadProfile => {
710 tracing::info!("Settings: Load profile action triggered");
711 state.with_mut::<SettingsMenuState, _, _>(|menu| {
713 menu.set_message(
714 "Profile load not yet implemented".to_string(),
715 MessageKind::Info,
716 );
717 });
718 }
719 ActionType::ResetToDefault => {
720 tracing::info!("Settings: Reset to default action triggered");
721 state.with_mut::<SettingsMenuState, _, _>(|menu| {
723 menu.set_message(
724 "Reset to default not yet implemented".to_string(),
725 MessageKind::Info,
726 );
727 });
728 }
729 }
730 ctx.request_render();
731 } else {
732 let change =
734 state.with_mut::<SettingsMenuState, _, _>(|menu| menu.toggle_selected());
735 if let Some(Some(change)) = change {
736 ctx.emit(OptionChanged::new(
737 change.key,
738 change.old_value,
739 change.new_value,
740 ChangeSource::SettingsMenu,
741 ));
742 ctx.request_render();
743 }
744 }
745 EventResult::Handled
746 });
747 }
748 }
749}