par_term/
tmux_session_picker_ui.rs1use crate::ui_constants::{
8 TMUX_PICKER_LIST_MAX_HEIGHT, TMUX_PICKER_WINDOW_DEFAULT_HEIGHT,
9 TMUX_PICKER_WINDOW_DEFAULT_WIDTH,
10};
11use egui::{Color32, Context, Frame, RichText, Window, epaint::Shadow};
12use std::process::Command;
13use std::time::Duration;
14
15const TMUX_LIST_TIMEOUT: Duration = Duration::from_secs(2);
18
19#[derive(Debug, Clone)]
21pub struct TmuxSessionInfo {
22 pub id: String,
24 pub name: String,
26 pub window_count: usize,
28 pub attached: bool,
30}
31
32#[derive(Debug, Clone)]
34pub enum SessionPickerAction {
35 None,
37 Attach(String),
39 CreateNew(Option<String>),
41}
42
43pub struct TmuxSessionPickerUI {
45 pub visible: bool,
47 sessions: Vec<TmuxSessionInfo>,
49 new_session_name: String,
51 error_message: Option<String>,
53 sessions_loaded: bool,
55}
56
57impl TmuxSessionPickerUI {
58 pub fn new() -> Self {
60 Self {
61 visible: false,
62 sessions: Vec::new(),
63 new_session_name: String::new(),
64 error_message: None,
65 sessions_loaded: false,
66 }
67 }
68
69 pub fn show_picker(&mut self) {
71 self.visible = true;
72 self.sessions_loaded = false; self.error_message = None;
74 self.new_session_name.clear();
75 }
76
77 pub fn hide(&mut self) {
79 self.visible = false;
80 }
81
82 pub fn toggle(&mut self) {
84 if self.visible {
85 self.hide();
86 } else {
87 self.show_picker();
88 }
89 }
90
91 pub fn refresh_sessions(&mut self, tmux_path: &str) {
93 match Self::list_tmux_sessions(tmux_path) {
94 Ok(sessions) => {
95 self.sessions = sessions;
96 self.error_message = None;
97 self.sessions_loaded = true;
98 }
99 Err(e) => {
100 self.sessions.clear();
101 self.error_message = Some(e);
102 self.sessions_loaded = true;
103 }
104 }
105 }
106
107 fn list_tmux_sessions(tmux_path: &str) -> Result<Vec<TmuxSessionInfo>, String> {
113 let mut cmd = Command::new(tmux_path);
114 cmd.args([
115 "list-sessions",
116 "-F",
117 "#{session_id}:#{session_name}:#{session_attached}:#{session_windows}",
118 ]);
119 let output = crate::process_timeout::output_with_timeout(&mut cmd, TMUX_LIST_TIMEOUT)
120 .map_err(|e| format!("Failed to run tmux: {}", e))?;
121
122 if !output.status.success() {
123 let stderr = &output.stderr;
124 if stderr.contains("no server running") || stderr.contains("no sessions") {
126 return Ok(Vec::new());
127 }
128 return Err(format!("tmux error: {}", stderr.trim()));
129 }
130
131 let mut sessions = Vec::new();
132
133 for line in output.stdout.lines() {
134 let parts: Vec<&str> = line.split(':').collect();
135 if parts.len() >= 4 {
136 sessions.push(TmuxSessionInfo {
137 id: parts[0].to_string(),
138 name: parts[1].to_string(),
139 attached: parts[2] == "1",
140 window_count: parts[3].parse().unwrap_or(0),
141 });
142 }
143 }
144
145 Ok(sessions)
146 }
147
148 pub fn show(&mut self, ctx: &Context, tmux_path: &str) -> SessionPickerAction {
150 if !self.visible {
151 return SessionPickerAction::None;
152 }
153
154 if !self.sessions_loaded {
156 self.refresh_sessions(tmux_path);
157 }
158
159 let mut action = SessionPickerAction::None;
160 let mut close_requested = false;
161
162 let mut style = (*ctx.global_style()).clone();
164 let solid_bg = Color32::from_rgba_unmultiplied(24, 24, 24, 255);
165 style.visuals.window_fill = solid_bg;
166 style.visuals.panel_fill = solid_bg;
167 ctx.set_global_style(style);
168
169 let mut open = true;
170 let viewport = ctx.input(|i| i.viewport_rect());
171
172 Window::new("tmux Sessions")
173 .resizable(true)
174 .default_width(TMUX_PICKER_WINDOW_DEFAULT_WIDTH)
175 .default_height(TMUX_PICKER_WINDOW_DEFAULT_HEIGHT)
176 .default_pos(viewport.center())
177 .pivot(egui::Align2::CENTER_CENTER)
178 .open(&mut open)
179 .frame(
180 Frame::window(&ctx.global_style())
181 .fill(solid_bg)
182 .stroke(egui::Stroke::NONE)
183 .shadow(Shadow {
184 offset: [0, 0],
185 blur: 0,
186 spread: 0,
187 color: Color32::TRANSPARENT,
188 }),
189 )
190 .show(ctx, |ui| {
191 if let Some(ref err) = self.error_message {
193 ui.colored_label(Color32::from_rgb(255, 100, 100), err);
194 ui.add_space(8.0);
195 }
196
197 ui.heading("Existing Sessions");
199 ui.separator();
200
201 if self.sessions.is_empty() {
202 ui.label(RichText::new("No tmux sessions found").italics());
203 } else {
204 egui::ScrollArea::vertical()
205 .max_height(TMUX_PICKER_LIST_MAX_HEIGHT)
206 .show(ui, |ui| {
207 for session in &self.sessions {
208 ui.horizontal(|ui| {
209 let name_text = if session.attached {
211 RichText::new(&session.name).strong()
212 } else {
213 RichText::new(&session.name)
214 };
215 ui.label(name_text);
216
217 ui.label(
219 RichText::new(format!(
220 "({} window{})",
221 session.window_count,
222 if session.window_count == 1 { "" } else { "s" }
223 ))
224 .weak(),
225 );
226
227 if session.attached {
229 ui.label(
230 RichText::new("(attached)")
231 .color(Color32::from_rgb(100, 200, 100)),
232 );
233 }
234
235 ui.with_layout(
236 egui::Layout::right_to_left(egui::Align::Center),
237 |ui| {
238 if ui.button("Attach").clicked() {
239 action = SessionPickerAction::Attach(
240 session.name.clone(),
241 );
242 close_requested = true;
243 }
244 },
245 );
246 });
247 }
248 });
249 }
250
251 ui.add_space(16.0);
252
253 if ui.button("Refresh").clicked() {
255 self.refresh_sessions(tmux_path);
256 }
257
258 ui.add_space(16.0);
259
260 ui.heading("Create New Session");
262 ui.separator();
263
264 ui.horizontal(|ui| {
265 ui.label("Session name:");
266 ui.text_edit_singleline(&mut self.new_session_name);
267 });
268
269 ui.add_space(8.0);
270
271 ui.horizontal(|ui| {
272 if ui.button("Create").clicked() {
273 let name = if self.new_session_name.is_empty() {
274 None
275 } else {
276 Some(self.new_session_name.clone())
277 };
278 action = SessionPickerAction::CreateNew(name);
279 close_requested = true;
280 }
281
282 ui.label(
283 RichText::new("(leave empty for auto-generated name)")
284 .small()
285 .weak(),
286 );
287 });
288 });
289
290 if !open || close_requested {
292 self.visible = false;
293 }
294
295 action
296 }
297}
298
299impl Default for TmuxSessionPickerUI {
300 fn default() -> Self {
301 Self::new()
302 }
303}