fractal/session_list/
mod.rs1use std::{cmp::Ordering, ffi::OsString};
2
3use gettextrs::gettext;
4use gtk::{gio, glib, glib::clone, prelude::*, subclass::prelude::*};
5use indexmap::map::IndexMap;
6use tracing::{error, info};
7
8mod failed_session;
9mod new_session;
10mod session_info;
11mod session_list_settings;
12
13pub(crate) use self::{
14 failed_session::*, new_session::*, session_info::*, session_list_settings::*,
15};
16use crate::{
17 prelude::*,
18 secret::{Secret, StoredSession},
19 session::{Session, SessionState},
20 spawn, spawn_tokio,
21 utils::{DataType, LoadingState},
22};
23
24mod imp {
25 use std::{
26 cell::{Cell, RefCell},
27 marker::PhantomData,
28 };
29
30 use super::*;
31
32 #[derive(Debug, Default, glib::Properties)]
33 #[properties(wrapper_type = super::SessionList)]
34 pub struct SessionList {
35 pub(super) list: RefCell<IndexMap<String, SessionInfo>>,
37 #[property(get, builder(LoadingState::default()))]
39 state: Cell<LoadingState>,
40 #[property(get, nullable)]
42 error: RefCell<Option<String>>,
43 #[property(get)]
45 settings: SessionListSettings,
46 #[property(get = Self::is_empty)]
48 is_empty: PhantomData<bool>,
49 }
50
51 #[glib::object_subclass]
52 impl ObjectSubclass for SessionList {
53 const NAME: &'static str = "SessionList";
54 type Type = super::SessionList;
55 type Interfaces = (gio::ListModel,);
56 }
57
58 #[glib::derived_properties]
59 impl ObjectImpl for SessionList {}
60
61 impl ListModelImpl for SessionList {
62 fn item_type(&self) -> glib::Type {
63 SessionInfo::static_type()
64 }
65
66 fn n_items(&self) -> u32 {
67 self.list.borrow().len() as u32
68 }
69
70 fn item(&self, position: u32) -> Option<glib::Object> {
71 self.list
72 .borrow()
73 .get_index(position as usize)
74 .map(|(_, v)| v.clone().upcast())
75 }
76 }
77
78 impl SessionList {
79 fn is_empty(&self) -> bool {
81 self.list.borrow().is_empty()
82 }
83
84 fn set_state(&self, state: LoadingState) {
86 if self.state.get() == state {
87 return;
88 }
89
90 self.state.set(state);
91 self.obj().notify_state();
92 }
93
94 fn set_error(&self, message: String) {
96 self.error.replace(Some(message));
97
98 self.obj().notify_error();
99 self.set_state(LoadingState::Error);
100 }
101
102 pub(super) fn insert(&self, session: impl IsA<SessionInfo>) -> usize {
108 let session = session.upcast();
109
110 if let Some(session) = session.downcast_ref::<Session>() {
111 session.connect_logged_out(clone!(
112 #[weak(rename_to = imp)]
113 self,
114 move |session| imp.remove(session.session_id())
115 ));
116 }
117
118 let was_empty = self.is_empty();
119
120 let (index, replaced) = self
121 .list
122 .borrow_mut()
123 .insert_full(session.session_id(), session);
124
125 let removed = replaced.is_some().into();
126
127 let obj = self.obj();
128 obj.items_changed(index as u32, removed, 1);
129
130 if was_empty {
131 obj.notify_is_empty();
132 }
133
134 index
135 }
136
137 fn remove(&self, session_id: &str) {
139 let removed = self.list.borrow_mut().shift_remove_full(session_id);
140
141 if let Some((position, ..)) = removed {
142 let obj = self.obj();
143 obj.items_changed(position as u32, 1, 0);
144
145 if self.is_empty() {
146 obj.notify_is_empty();
147 }
148 }
149 }
150
151 pub(super) async fn restore_sessions(&self) {
153 if self.state.get() >= LoadingState::Loading {
154 return;
155 }
156
157 self.set_state(LoadingState::Loading);
158
159 let mut sessions = match Secret::restore_sessions().await {
160 Ok(sessions) => sessions,
161 Err(error) => {
162 let message = format!(
163 "{}\n\n{}",
164 gettext("Could not restore previous sessions"),
165 error.to_user_facing(),
166 );
167
168 self.set_error(message);
169 return;
170 }
171 };
172
173 self.settings.load();
174 let session_ids = self.settings.session_ids();
175
176 sessions.sort_by(|a, b| {
178 let pos_a = session_ids.get_index_of(&a.id);
179 let pos_b = session_ids.get_index_of(&b.id);
180
181 match (pos_a, pos_b) {
182 (Some(pos_a), Some(pos_b)) => pos_a.cmp(&pos_b),
183 (Some(_), None) => Ordering::Greater,
185 (None, Some(_)) => Ordering::Less,
186 _ => Ordering::Equal,
187 }
188 });
189
190 let mut directories = match self.data_directories(sessions.len()).await {
194 Ok(directories) => directories,
195 Err(error) => {
196 error!("Could not access data directory: {error}");
197 let message = format!(
198 "{}\n\n{}",
199 gettext("Could not restore previous sessions"),
200 gettext("An unexpected error happened while accessing the data directory"),
201 );
202
203 self.set_error(message);
204 return;
205 }
206 };
207
208 for stored_session in sessions {
209 if let Some(pos) = directories
210 .iter()
211 .position(|dir_name| dir_name == stored_session.id.as_str())
212 {
213 directories.swap_remove(pos);
214 info!(
215 "Restoring previous session {} for user {}",
216 stored_session.id, stored_session.user_id,
217 );
218 self.insert(NewSession::new(&stored_session));
219
220 spawn!(
221 glib::Priority::DEFAULT_IDLE,
222 clone!(
223 #[weak(rename_to = obj)]
224 self,
225 async move {
226 obj.restore_stored_session(&stored_session).await;
227 }
228 )
229 );
230 } else {
231 info!(
232 "Ignoring session {} for user {}: no data directory",
233 stored_session.id, stored_session.user_id,
234 );
235 }
236 }
237
238 self.set_state(LoadingState::Ready);
239 }
240
241 async fn data_directories(&self, capacity: usize) -> std::io::Result<Vec<OsString>> {
243 let data_path = DataType::Persistent.dir_path();
244
245 if !data_path.try_exists()? {
246 return Ok(Vec::new());
247 }
248
249 spawn_tokio!(async move {
250 let mut read_dir = tokio::fs::read_dir(data_path).await?;
251 let mut directories = Vec::with_capacity(capacity);
252
253 loop {
254 let Some(entry) = read_dir.next_entry().await? else {
255 break;
257 };
258
259 if !entry.file_type().await?.is_dir() {
260 continue;
262 }
263
264 directories.push(entry.file_name());
265 }
266
267 std::io::Result::Ok(directories)
268 })
269 .await
270 .expect("task was not aborted")
271 }
272
273 async fn restore_stored_session(&self, session_info: &StoredSession) {
275 let settings = self.settings.get_or_create(&session_info.id);
276 match Session::new(session_info.clone(), settings).await {
277 Ok(session) => {
278 session.prepare().await;
279 self.insert(session);
280 }
281 Err(error) => {
282 error!("Could not restore previous session: {error}");
283 self.insert(FailedSession::new(session_info, error));
284 }
285 }
286 }
287 }
288}
289
290glib::wrapper! {
291 pub struct SessionList(ObjectSubclass<imp::SessionList>)
293 @implements gio::ListModel;
294}
295
296impl SessionList {
297 pub fn new() -> Self {
299 glib::Object::new()
300 }
301
302 pub(crate) fn has_session_ready(&self) -> bool {
304 self.imp()
305 .list
306 .borrow()
307 .values()
308 .filter_map(|s| s.downcast_ref::<Session>())
309 .any(|s| s.state() == SessionState::Ready)
310 }
311
312 pub(crate) fn get(&self, session_id: &str) -> Option<SessionInfo> {
314 self.imp().list.borrow().get(session_id).cloned()
315 }
316
317 pub(crate) fn index(&self, session_id: &str) -> Option<usize> {
319 self.imp().list.borrow().get_index_of(session_id)
320 }
321
322 pub(crate) fn first(&self) -> Option<SessionInfo> {
324 self.imp().list.borrow().first().map(|(_, v)| v.clone())
325 }
326
327 pub(crate) fn insert(&self, session: impl IsA<SessionInfo>) -> usize {
333 self.imp().insert(session)
334 }
335
336 pub(crate) async fn restore_sessions(&self) {
338 self.imp().restore_sessions().await;
339 }
340}
341
342impl Default for SessionList {
343 fn default() -> Self {
344 Self::new()
345 }
346}