1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
use crate::config::Config;
use crate::error::{Result, TmuxrsError};
use crate::tmux::TmuxCommand;
use std::path::{Path, PathBuf};
/// Session manager for tmuxrs
#[derive(Default)]
pub struct SessionManager;
impl SessionManager {
/// Create a new session manager
pub fn new() -> Self {
Self
}
/// Expand tilde (~) and environment variables in paths using shellexpand
fn expand_path(path: &str) -> Result<PathBuf> {
// Try full expansion first (handles both tilde and environment variables)
match shellexpand::full(path) {
Ok(expanded) => Ok(PathBuf::from(expanded.as_ref())),
Err(_) => {
// Fallback: try basic tilde expansion only
let expanded = shellexpand::tilde(path);
Ok(PathBuf::from(expanded.as_ref()))
}
}
}
/// Start a session with optional explicit name
pub fn start_session(&self, name: Option<&str>, config_dir: Option<&Path>) -> Result<String> {
// Use default behavior: attach=true, append=false
self.start_session_with_options(name, config_dir, true, false)
}
/// Start a session with full options control
pub fn start_session_with_options(
&self,
name: Option<&str>,
config_dir: Option<&Path>,
attach: bool,
append: bool,
) -> Result<String> {
let session_name = match name {
Some(n) => n.to_string(),
None => Config::detect_session_name(None)?,
};
// Check if session already exists
if TmuxCommand::session_exists(&session_name)? {
if append {
// TODO: Implement append functionality in Phase 2
return Err(TmuxrsError::TmuxError(
"Append functionality not yet implemented".to_string(),
));
} else if attach {
// Attach to existing session
match TmuxCommand::attach_session(&session_name) {
Ok(()) => {
// This line should never be reached in practice because
// successful attach takes over the terminal process
return Ok(format!("Attached to existing session '{session_name}'"));
}
Err(err) => {
// Attach failed - could be no TTY, session doesn't exist, etc.
return Err(TmuxrsError::TmuxError(format!(
"Failed to attach to session '{session_name}': {err}"
)));
}
}
} else {
return Ok(format!("Session '{session_name}' already exists"));
}
}
// Load configuration
let config = if let Some(config_dir) = config_dir {
// Load from custom config directory
let config_file = config_dir.join(format!("{session_name}.yml"));
Config::parse_file(&config_file)?
} else {
Config::load(&session_name)?
};
// Create session
let root_dir = config.root.as_deref().unwrap_or("~");
let root_path = Self::expand_path(root_dir)?;
TmuxCommand::new_session(&session_name, &root_path)?;
// Create windows
for (index, window_config) in config.windows.iter().enumerate() {
match window_config {
crate::config::WindowConfig::Simple(command) => {
let window_name = format!("window-{}", index + 1);
// Create window without command to allow proper shell initialization
TmuxCommand::new_window(
&session_name,
&window_name,
None, // No command - let shell initialize properly
Some(&root_path),
)?;
// Send command after window is created
if !command.trim().is_empty() {
TmuxCommand::send_keys(&session_name, &window_name, command)?;
}
}
crate::config::WindowConfig::Complex { window } => {
for (window_name, command) in window {
// Create window without command to allow proper shell initialization
TmuxCommand::new_window(
&session_name,
window_name,
None, // No command - let shell initialize properly
Some(&root_path),
)?;
// Send command after window is created
if !command.trim().is_empty() {
TmuxCommand::send_keys(&session_name, window_name, command)?;
}
}
}
crate::config::WindowConfig::WithLayout { window } => {
for (window_name, layout_config) in window {
// Create the window without command to allow proper shell initialization
TmuxCommand::new_window(
&session_name,
window_name,
None, // No command - let shell initialize properly
Some(&root_path),
)?;
// Send first pane command if not empty
let first_pane = layout_config.panes.first().ok_or_else(|| {
TmuxrsError::TmuxError(
"Window layout must have at least one pane".to_string(),
)
})?;
if !first_pane.trim().is_empty() {
TmuxCommand::send_keys(&session_name, window_name, first_pane)?;
}
// Add additional panes by splitting
for (pane_index, pane_command) in
layout_config.panes.iter().skip(1).enumerate()
{
// Create split without command to allow proper shell initialization
TmuxCommand::split_window_horizontal(
&session_name,
window_name,
"", // Empty command - shell will initialize properly
Some(&root_path),
)?;
// Send command to the new pane after it's created
// Pane indices start at 0, first pane is 0, second is 1, etc.
let target_pane_index = pane_index + 1; // +1 because we skipped the first pane
if !pane_command.trim().is_empty() {
TmuxCommand::send_keys_to_pane(
&session_name,
window_name,
target_pane_index,
pane_command,
)?;
}
}
// Apply layout if specified
if let Some(layout) = &layout_config.layout {
TmuxCommand::select_layout(&session_name, window_name, layout)?;
}
}
}
}
}
// Handle attachment
if attach {
match TmuxCommand::attach_session(&session_name) {
Ok(()) => {
// This line should never be reached in practice because
// successful attach takes over the terminal process
Ok(format!("Started and attached to session '{session_name}'"))
}
Err(err) => {
// Attach failed - provide helpful error message
Err(TmuxrsError::TmuxError(format!(
"Started session '{session_name}' but failed to attach: {err}"
)))
}
}
} else {
Ok(format!("Started detached session '{session_name}'"))
}
}
/// Start a session detecting name from directory
#[allow(dead_code)]
pub fn start_session_from_directory(
&self,
directory: &Path,
config_dir: Option<&Path>,
) -> Result<String> {
let session_name = Config::detect_session_name(Some(directory))?;
self.start_session(Some(&session_name), config_dir)
}
/// List available configurations
pub fn list_configs(&self, config_dir: Option<&Path>) -> Result<Vec<Config>> {
let search_dir = match config_dir {
Some(dir) => dir.to_path_buf(),
None => {
let home_dir = dirs::home_dir().ok_or_else(|| {
TmuxrsError::ConfigNotFound("Could not find home directory".to_string())
})?;
home_dir.join(".config").join("tmuxrs")
}
};
if !search_dir.exists() {
return Ok(Vec::new());
}
let mut configs = Vec::new();
for entry in std::fs::read_dir(&search_dir)? {
let entry = entry?;
let path = entry.path();
if path.is_file()
&& path
.extension()
.is_some_and(|ext| ext == "yml" || ext == "yaml")
{
match Config::parse_file(&path) {
Ok(config) => configs.push(config),
Err(_) => continue, // Skip invalid config files
}
}
}
Ok(configs)
}
/// Stop a session
pub fn stop_session(&self, name: &str) -> Result<String> {
// Check if session exists first
if !TmuxCommand::session_exists(name)? {
return Err(TmuxrsError::TmuxError(format!(
"Session '{name}' does not exist"
)));
}
TmuxCommand::kill_session(name)?;
Ok(format!("Stopped session '{name}'"))
}
}