tms/
session.rs

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