1use std::{
2 collections::HashMap,
3 path::{Path, PathBuf},
4};
5
6use error_stack::ResultExt;
7
8use crate::{
9 configs::{Config, VcsProviders},
10 dirty_paths::DirtyUtf8Path,
11 error::TmsError,
12 repos::{find_repos, find_submodules, LazyRepoProvider},
13 tmux::Tmux,
14 Result,
15};
16
17pub struct Session {
18 pub name: String,
19 pub session_type: SessionType,
20}
21
22pub enum SessionType {
23 Git(LazyRepoProvider),
24 Bookmark(PathBuf),
25}
26
27impl Session {
28 pub fn new(name: String, session_type: SessionType) -> Self {
29 Session { name, session_type }
30 }
31
32 pub fn path(&self) -> &Path {
33 match &self.session_type {
34 SessionType::Git(provider) => &provider.path,
35 SessionType::Bookmark(path) => path,
36 }
37 }
38
39 pub fn switch_to(&self, tmux: &Tmux, config: &Config) -> Result<()> {
40 match &self.session_type {
41 SessionType::Git(repo) => self.switch_to_repo_session(repo, tmux, config),
42 SessionType::Bookmark(path) => self.switch_to_bookmark_session(tmux, path, config),
43 }
44 }
45
46 fn switch_to_repo_session(
47 &self,
48 repo: &LazyRepoProvider,
49 tmux: &Tmux,
50 config: &Config,
51 ) -> Result<()> {
52 let repo = repo.resolve()?;
53 let path = if repo.is_bare() {
54 repo.path().to_path_buf().to_string()?
55 } else {
56 repo.work_dir()
57 .expect("bare repositories should all have parent directories")
58 .canonicalize()
59 .change_context(TmsError::IoError)?
60 .to_string()?
61 };
62 let session_name = self.name.replace('.', "_");
63
64 if !tmux.session_exists(&session_name) {
65 tmux.new_session(Some(&session_name), Some(&path));
66 tmux.set_up_tmux_env(repo, &session_name)?;
67 tmux.run_session_create_script(self.path(), &session_name, config)?;
68 }
69
70 tmux.switch_to_session(&session_name);
71
72 Ok(())
73 }
74
75 fn switch_to_bookmark_session(&self, tmux: &Tmux, path: &Path, config: &Config) -> Result<()> {
76 let session_name = self.name.replace('.', "_");
77
78 if !tmux.session_exists(&session_name) {
79 tmux.new_session(Some(&session_name), path.to_str());
80 tmux.run_session_create_script(path, &session_name, config)?;
81 }
82
83 tmux.switch_to_session(&session_name);
84
85 Ok(())
86 }
87}
88
89pub trait SessionContainer {
90 fn find_session(&self, name: &str) -> Option<&Session>;
91 fn insert_session(&mut self, name: String, repo: Session);
92 fn list(&self) -> Vec<String>;
93}
94
95impl SessionContainer for HashMap<String, Session> {
96 fn find_session(&self, name: &str) -> Option<&Session> {
97 self.get(name)
98 }
99
100 fn insert_session(&mut self, name: String, session: Session) {
101 self.insert(name, session);
102 }
103
104 fn list(&self) -> Vec<String> {
105 let mut list: Vec<String> = self.keys().map(|s| s.to_owned()).collect();
106 list.sort();
107
108 list
109 }
110}
111
112pub fn create_sessions(config: &Config) -> Result<impl SessionContainer> {
113 let mut sessions = find_repos(config)?;
114 sessions = append_bookmarks(config, sessions)?;
115
116 let sessions = generate_session_container(sessions, config)?;
117
118 Ok(sessions)
119}
120
121fn generate_session_container(
122 mut sessions: HashMap<String, Vec<Session>>,
123 config: &Config,
124) -> Result<impl SessionContainer> {
125 let mut ret = HashMap::new();
126
127 for list in sessions.values_mut() {
128 if list.len() == 1 {
129 let session = list.pop().unwrap();
130 insert_session(&mut ret, session, config)?;
131 } else {
132 let deduplicated = deduplicate_sessions(list);
133
134 for session in deduplicated {
135 insert_session(&mut ret, session, config)?;
136 }
137 }
138 }
139
140 Ok(ret)
141}
142
143fn insert_session(
144 sessions: &mut impl SessionContainer,
145 session: Session,
146 config: &Config,
147) -> Result<()> {
148 let visible_name = if config.display_full_path == Some(true) {
149 session.path().display().to_string()
150 } else {
151 session.name.clone()
152 };
153 if let SessionType::Git(repo) = &session.session_type {
154 if matches!(
155 (config.search_submodules, repo.provider),
156 (Some(true), VcsProviders::Git),
157 ) {
158 if let Ok(Some(submodules)) = repo.resolve().and_then(|repo| repo.submodules()) {
159 find_submodules(submodules, &visible_name, sessions, config)?;
160 }
161 }
162 }
163 sessions.insert_session(visible_name, session);
164 Ok(())
165}
166
167fn deduplicate_sessions(duplicate_sessions: &mut Vec<Session>) -> Vec<Session> {
168 let mut depth = 1;
169 let mut deduplicated = Vec::new();
170 while let Some(current_session) = duplicate_sessions.pop() {
171 let mut equal = true;
172 let current_path = current_session.path();
173 let mut current_depth = 1;
174
175 while equal {
176 equal = false;
177 if let Some(current_str) = current_path.iter().rev().nth(current_depth) {
178 for session in &mut *duplicate_sessions {
179 if let Some(str) = session.path().iter().rev().nth(current_depth) {
180 if str == current_str {
181 current_depth += 1;
182 equal = true;
183 break;
184 }
185 }
186 }
187 }
188 }
189
190 deduplicated.push(current_session);
191 depth = depth.max(current_depth);
192 }
193
194 for session in &mut deduplicated {
195 session.name = {
196 let mut count = depth + 1;
197 let mut iterator = session.path().iter().rev();
198 let mut str = String::new();
199
200 while count > 0 {
201 if let Some(dir) = iterator.next() {
202 if str.is_empty() {
203 str = dir.to_string_lossy().to_string();
204 } else {
205 str = format!("{}/{}", dir.to_string_lossy(), str);
206 }
207 count -= 1;
208 } else {
209 count = 0;
210 }
211 }
212
213 str
214 };
215 }
216
217 deduplicated
218}
219
220fn append_bookmarks(
221 config: &Config,
222 mut sessions: HashMap<String, Vec<Session>>,
223) -> Result<HashMap<String, Vec<Session>>> {
224 let bookmarks = config.bookmark_paths();
225
226 for path in bookmarks {
227 let session_name = path
228 .file_name()
229 .expect("The file name doesn't end in `..`")
230 .to_string()?;
231 let session = Session::new(session_name, SessionType::Bookmark(path));
232 if let Some(list) = sessions.get_mut(&session.name) {
233 list.push(session);
234 } else {
235 sessions.insert(session.name.clone(), vec![session]);
236 }
237 }
238
239 Ok(sessions)
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245
246 #[test]
247 fn verify_session_name_deduplication() {
248 let mut test_sessions = vec![
249 Session::new(
250 "test".into(),
251 SessionType::Bookmark("/search/path/to/proj1/test".into()),
252 ),
253 Session::new(
254 "test".into(),
255 SessionType::Bookmark("/search/path/to/proj2/test".into()),
256 ),
257 Session::new(
258 "test".into(),
259 SessionType::Bookmark("/other/path/to/projects/proj2/test".into()),
260 ),
261 ];
262
263 let deduplicated = deduplicate_sessions(&mut test_sessions);
264
265 assert_eq!(deduplicated[0].name, "projects/proj2/test");
266 assert_eq!(deduplicated[1].name, "to/proj2/test");
267 assert_eq!(deduplicated[2].name, "to/proj1/test");
268 }
269}