themes: use the core module for io
[git-cola.git] / cola / settings.py
blobfad7fcd3f5d0d75f740e2ea666d110f64118a53a
1 """Save settings, bookmarks, etc."""
2 from __future__ import absolute_import, division, print_function, unicode_literals
3 import json
4 import os
5 import sys
7 from . import core
8 from . import display
9 from . import git
10 from . import resources
13 def mkdict(obj):
14 """Transform None and non-dicts into dicts"""
15 if isinstance(obj, dict):
16 value = obj
17 else:
18 value = {}
19 return value
22 def mklist(obj):
23 """Transform None and non-lists into lists"""
24 if isinstance(obj, list):
25 value = obj
26 elif isinstance(obj, tuple):
27 value = list(obj)
28 else:
29 value = []
30 return value
33 def read_json(path):
34 try:
35 with core.xopen(path, 'rt') as fp:
36 return mkdict(json.load(fp))
37 except (ValueError, TypeError, OSError, IOError): # bad path or json
38 return {}
41 def write_json(values, path):
42 try:
43 parent = os.path.dirname(path)
44 if not core.isdir(parent):
45 core.makedirs(parent)
46 with core.xopen(path, 'wt') as fp:
47 json.dump(values, fp, indent=4)
48 except (ValueError, TypeError, OSError, IOError):
49 sys.stderr.write('git-cola: error writing "%s"\n' % path)
52 class Settings(object):
53 config_path = resources.config_home('settings')
54 bookmarks = property(lambda self: mklist(self.values['bookmarks']))
55 gui_state = property(lambda self: mkdict(self.values['gui_state']))
56 recent = property(lambda self: mklist(self.values['recent']))
57 copy_formats = property(lambda self: mklist(self.values['copy_formats']))
59 def __init__(self, verify=git.is_git_worktree):
60 """Load existing settings if they exist"""
61 self.values = {
62 'bookmarks': [],
63 'gui_state': {},
64 'recent': [],
65 'copy_formats': [],
67 self.verify = verify
69 def remove_missing_bookmarks(self):
70 """Remove "favorites" bookmarks that no longer exist"""
71 missing_bookmarks = []
72 for bookmark in self.bookmarks:
73 if not self.verify(bookmark['path']):
74 missing_bookmarks.append(bookmark)
76 for bookmark in missing_bookmarks:
77 try:
78 self.bookmarks.remove(bookmark)
79 except ValueError:
80 pass
82 def remove_missing_recent(self):
83 """Remove "recent" repositories that no longer exist"""
84 missing_recent = []
85 for recent in self.recent:
86 if not self.verify(recent['path']):
87 missing_recent.append(recent)
89 for recent in missing_recent:
90 try:
91 self.recent.remove(recent)
92 except ValueError:
93 pass
95 def add_bookmark(self, path, name):
96 """Adds a bookmark to the saved settings"""
97 bookmark = {'path': display.normalize_path(path), 'name': name}
98 if bookmark not in self.bookmarks:
99 self.bookmarks.append(bookmark)
101 def remove_bookmark(self, path, name):
102 """Remove a bookmark"""
103 bookmark = {'path': display.normalize_path(path), 'name': name}
104 try:
105 self.bookmarks.remove(bookmark)
106 except ValueError:
107 pass
109 def rename_bookmark(self, path, name, new_name):
110 return rename_entry(self.bookmarks, path, name, new_name)
112 def add_recent(self, path, max_recent):
113 normalize = display.normalize_path
114 path = normalize(path)
115 try:
116 index = [normalize(recent['path']) for recent in self.recent].index(path)
117 entry = self.recent.pop(index)
118 except (IndexError, ValueError):
119 entry = {
120 'name': os.path.basename(path),
121 'path': path,
123 self.recent.insert(0, entry)
124 if len(self.recent) > max_recent:
125 self.recent.pop()
127 def remove_recent(self, path):
128 """Removes an item from the recent items list"""
129 normalize = display.normalize_path
130 path = normalize(path)
131 try:
132 index = [normalize(recent.get('path', '')) for recent in self.recent].index(
133 path
135 except ValueError:
136 return
137 try:
138 self.recent.pop(index)
139 except IndexError:
140 return
142 def rename_recent(self, path, name, new_name):
143 return rename_entry(self.recent, path, name, new_name)
145 def path(self):
146 return self.config_path
148 def save(self):
149 write_json(self.values, self.path())
151 def load(self, path=None):
152 self.values.update(self.asdict(path=path))
153 self.upgrade_settings()
154 return True
156 @staticmethod
157 def read(verify=git.is_git_worktree):
158 """Load settings from disk"""
159 settings = Settings(verify=verify)
160 settings.load()
161 return settings
163 def upgrade_settings(self):
164 """Upgrade git-cola settings"""
165 # Upgrade bookmarks to the new dict-based bookmarks format.
166 normalize = display.normalize_path
167 if self.bookmarks and not isinstance(self.bookmarks[0], dict):
168 bookmarks = [
169 dict(name=os.path.basename(path), path=normalize(path))
170 for path in self.bookmarks
172 self.values['bookmarks'] = bookmarks
174 if self.recent and not isinstance(self.recent[0], dict):
175 recent = [
176 dict(name=os.path.basename(path), path=normalize(path))
177 for path in self.recent
179 self.values['recent'] = recent
181 def asdict(self, path=None):
182 if not path:
183 path = self.path()
184 if core.exists(path):
185 return read_json(path)
186 # We couldn't find ~/.config/git-cola, try ~/.cola
187 values = {}
188 path = os.path.join(core.expanduser('~'), '.cola')
189 if core.exists(path):
190 json_values = read_json(path)
191 for key in self.values:
192 try:
193 values[key] = json_values[key]
194 except KeyError:
195 pass
196 # Ensure that all stored bookmarks use normalized paths ("/" only).
197 normalize = display.normalize_path
198 for entry in values.get('bookmarks', []):
199 entry['path'] = normalize(entry['path'])
200 for entry in values.get('recent', []):
201 entry['path'] = normalize(entry['path'])
202 return values
204 def save_gui_state(self, gui):
205 """Saves settings for a cola view"""
206 name = gui.name()
207 self.gui_state[name] = mkdict(gui.export_state())
208 self.save()
210 def get_gui_state(self, gui):
211 """Returns the saved state for a gui"""
212 try:
213 state = mkdict(self.gui_state[gui.name()])
214 except KeyError:
215 state = self.gui_state[gui.name()] = {}
216 return state
219 def rename_entry(entries, path, name, new_name):
220 normalize = display.normalize_path
221 path = normalize(path)
222 entry = {'name': name, 'path': path}
223 try:
224 index = entries.index(entry)
225 except ValueError:
226 return False
228 if all(item['name'] != new_name for item in entries):
229 entries[index]['name'] = new_name
230 result = True
231 else:
232 result = False
233 return result
236 class Session(Settings):
237 """Store per-session settings
239 XDG sessions are created by the QApplication::commitData() callback.
240 These sessions are stored once, and loaded once. They are deleted once
241 loaded. The behavior of path() is such that it forgets its session path()
242 and behaves like a return Settings object after the session has been
243 loaded once.
245 Once the session is loaded, it is removed and further calls to save()
246 will save to the usual $XDG_CONFIG_HOME/git-cola/settings location.
250 _sessions_dir = resources.config_home('sessions')
252 repo = property(lambda self: self.values['repo'])
254 def __init__(self, session_id, repo=None):
255 Settings.__init__(self)
256 self.session_id = session_id
257 self.values.update({'repo': repo})
258 self.expired = False
260 def session_path(self):
261 """The session-specific session file"""
262 return os.path.join(self._sessions_dir, self.session_id)
264 def path(self):
265 base_path = super(Session, self).path()
266 if self.expired:
267 path = base_path
268 else:
269 path = self.session_path()
270 if not os.path.exists(path):
271 path = base_path
272 return path
274 def load(self, path=None):
275 """Load the session and expire it for future loads
277 The session should be loaded only once. We remove the session file
278 when it's loaded, and set the session to be expired. This results in
279 future calls to load() and save() using the default Settings path
280 rather than the session-specific path.
282 The use case for sessions is when the user logs out with apps running.
283 We will restore their state, and if they then shutdown, it'll be just
284 like a normal shutdown and settings will be stored to
285 ~/.config/git-cola/settings instead of the session path.
287 This is accomplished by "expiring" the session after it has
288 been loaded initially.
291 result = super(Session, self).load(path=path)
292 # This is the initial load, so expire the session and remove the
293 # session state file. Future calls will be equivalent to
294 # Settings.load().
295 if not self.expired:
296 self.expired = True
297 path = self.session_path()
298 if core.exists(path):
299 try:
300 os.unlink(path)
301 except (OSError, ValueError):
302 pass
303 return True
304 return False
306 return result
308 def update(self):
309 """Reload settings from the base settings path"""
310 # This method does not expire the session.
311 path = super(Session, self).path()
312 return super(Session, self).load(path=path)