1use std::collections::HashMap;
4use std::io::{self, stdout};
5
6use crossterm::cursor;
7use crossterm::execute;
8use crossterm::terminal::{self, DisableLineWrap, EnableLineWrap};
9use ratatui::Terminal;
10use ratatui::backend::CrosstermBackend;
11
12use crate::sidebar::context::ContextManager;
13use crate::sidebar::event::{self, Action};
14use crate::sidebar::state::{StateDetector, WindowState};
15use crate::sidebar::ui::SidebarWidget;
16use crate::tmux::{self, WindowInfo};
17
18struct SidebarApp {
21 windows: Vec<WindowInfo>,
22 states: HashMap<u32, WindowState>,
23 selected: usize,
24 tick: u64,
25 detector: StateDetector,
26 context_mgr: ContextManager,
27}
28
29const REFRESH_EVERY: u64 = 2;
32
33pub fn run() -> Result<(), String> {
36 let mut stdout = stdout();
38 execute!(stdout, cursor::Hide, DisableLineWrap).map_err(|e| format!("terminal: {e}"))?;
39 terminal::enable_raw_mode().map_err(|e| format!("terminal: {e}"))?;
40
41 let result = run_loop();
42
43 terminal::disable_raw_mode().ok();
45 execute!(stdout, cursor::Show, EnableLineWrap).ok();
46
47 result
48}
49
50fn run_loop() -> Result<(), String> {
53 let backend = CrosstermBackend::new(io::stdout());
54 let mut terminal = Terminal::new(backend).map_err(|e| format!("terminal: {e}"))?;
55
56 let mut app = SidebarApp {
57 windows: Vec::new(),
58 states: HashMap::new(),
59 selected: 0,
60 tick: 0,
61 detector: StateDetector::new(),
62 context_mgr: ContextManager::new(),
63 };
64
65 loop {
66 if app.tick % REFRESH_EVERY == 0 {
68 refresh_windows(&mut app);
69 }
70
71 app.states = app.detector.detect(&app.windows);
73
74 let detector = &app.detector;
76 app.context_mgr.tick(
77 &app.windows,
78 &app.states,
79 app.selected,
80 &|idx| detector.pane_id(idx).map(str::to_string),
81 &|idx| detector.cwd(idx).map(str::to_string),
82 );
83
84 let context = app
86 .windows
87 .get(app.selected)
88 .and_then(|win| app.context_mgr.get(&win.name));
89 let context_loading = app
90 .windows
91 .get(app.selected)
92 .is_some_and(|win| app.context_mgr.is_loading(&win.name));
93 let context_error = app
94 .windows
95 .get(app.selected)
96 .and_then(|win| app.context_mgr.get_error(&win.name));
97
98 terminal
100 .draw(|frame| {
101 let area = frame.area();
102 let widget = SidebarWidget {
103 windows: &app.windows,
104 states: &app.states,
105 selected: app.selected,
106 tick: app.tick,
107 context,
108 context_loading,
109 context_error,
110 };
111 frame.render_widget(widget, area);
112 })
113 .map_err(|e| format!("render: {e}"))?;
114
115 let actions = event::poll();
117 let mut moved = false;
118
119 for action in actions {
120 match action {
121 Action::Up => {
122 if app.selected > 0 {
123 app.selected -= 1;
124 moved = true;
125 }
126 }
127 Action::Down => {
128 if app.selected + 1 < app.windows.len() {
129 app.selected += 1;
130 moved = true;
131 }
132 }
133 Action::Select => {
134 if let Some(win) = app.windows.get(app.selected) {
135 let _ = tmux::select_window(win.index);
136 refresh_windows(&mut app);
137 app.tick = 0;
138 continue;
139 }
140 }
141 Action::Quit => return Ok(()),
142 Action::Tick => {}
143 }
144 }
145
146 if moved {
148 if let Some(win) = app.windows.get(app.selected) {
149 let _ = tmux::select_window_sidebar(win.index);
150 }
151 app.tick = 1;
153 } else {
154 app.tick += 1;
155 }
156 }
157}
158
159fn refresh_windows(app: &mut SidebarApp) {
160 if let Ok(windows) = tmux::list_windows() {
161 let active_pos = windows.iter().position(|w| w.is_active).unwrap_or(0);
163
164 app.selected = active_pos;
165 app.windows = windows;
166
167 if app.selected >= app.windows.len() && !app.windows.is_empty() {
169 app.selected = app.windows.len() - 1;
170 }
171 }
172}