Skip to main content

tms/
session.rs

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}