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