12 from config import settings |
12 from config import settings |
13 import objc |
13 import objc |
14 |
14 |
15 logger=borgend.logger.getChild(__name__) |
15 logger=borgend.logger.getChild(__name__) |
16 |
16 |
17 traynames={ |
17 traynames_ok={ |
18 backup.INACTIVE: 'B.', |
18 backup.State.INACTIVE: 'B.', |
19 backup.SCHEDULED: 'B.', |
19 backup.State.SCHEDULED: 'B.', |
20 backup.QUEUED: 'B:', |
20 backup.State.QUEUED: 'B:', |
21 backup.ACTIVE: 'B!', |
21 backup.State.ACTIVE: 'B!', |
22 backup.BUSY: 'B⦙', |
|
23 backup.OFFLINE: 'B⦙', |
|
24 backup.ERRORS: 'B?' |
|
25 } |
22 } |
26 |
23 |
27 statestring={ |
24 traynames_errors={ |
28 backup.INACTIVE: 'inactive', |
25 # The first one should never be used |
29 backup.SCHEDULED: 'scheduled', |
26 backup.Errors.OK: traynames_ok[backup.State.INACTIVE], |
30 backup.QUEUED: 'queued', |
27 backup.Errors.BUSY: 'B⦙', |
31 backup.ACTIVE: 'active', |
28 backup.Errors.OFFLINE: 'B⦙', |
32 backup.BUSY: 'busy', |
29 backup.Errors.ERRORS: 'B?' |
33 backup.OFFLINE: 'offline', |
|
34 backup.ERRORS: 'errors' |
|
35 } |
30 } |
|
31 |
|
32 def trayname(ste): |
|
33 state=ste[0] |
|
34 errors=ste[1] |
|
35 if not errors.ok(): |
|
36 return traynames_errors[errors] |
|
37 else: |
|
38 return traynames_ok[state] |
|
39 |
|
40 def combine_state(a, b): |
|
41 return (max(a[0], b[0]), max(a[1], b[1])) |
36 |
42 |
37 # Refresh the menu at most once a second to reduce flicker |
43 # Refresh the menu at most once a second to reduce flicker |
38 refresh_interval=1.0 |
44 refresh_interval=1.0 |
39 |
45 |
40 # Workaround to rumps brokenness; |
46 # Workaround to rumps brokenness; |
68 return '{0:.2f}GB'.format(B/GB) |
74 return '{0:.2f}GB'.format(B/GB) |
69 elif TB <= B: |
75 elif TB <= B: |
70 return '{0:.2f}TB'.format(B/TB) |
76 return '{0:.2f}TB'.format(B/TB) |
71 |
77 |
72 def make_title(status): |
78 def make_title(status): |
73 state=status['state'] |
79 def add_detail(detail, new): |
74 detail='' |
80 if detail: |
75 if status['type']=='scheduled': |
81 return "%s; %s" % (detail, new) |
|
82 else: |
|
83 return new |
|
84 |
|
85 errors=status['errors'] |
|
86 detail=None |
|
87 |
|
88 if not errors.ok(): |
|
89 detail=add_detail(detail, str(errors)) |
|
90 |
|
91 if status['state']==backup.State.SCHEDULED: |
76 # Operation scheduled |
92 # Operation scheduled |
77 when=status['when'] |
93 when=status['when'] |
78 now=time.time() |
94 now=time.time() |
79 if when<now: |
95 if when<now: |
80 whenstr='overdue' |
96 whenstr='overdue' |
89 if twhen.tm_sec>30: |
105 if twhen.tm_sec>30: |
90 # Round up minute display to avoid user confusion |
106 # Round up minute display to avoid user confusion |
91 twhen=time.localtime(when+30) |
107 twhen=time.localtime(when+30) |
92 whenstr='at %02d:%02d' % (twhen.tm_hour, twhen.tm_min) |
108 whenstr='at %02d:%02d' % (twhen.tm_hour, twhen.tm_min) |
93 |
109 |
94 detail='' |
110 this_detail='' |
95 if state>=backup.BUSY and state in statestring: |
|
96 detail=statestring[state] + '; ' |
|
97 if status['detail']!='normal': |
111 if status['detail']!='normal': |
98 detail=detail+status['detail']+' ' |
112 this_detail=status['detail'] + ' ' |
99 title="%s (%s%s %s)" % (status['name'], detail, status['operation'], whenstr) |
113 |
100 elif status['type']=='queued': |
114 when_how_sched= "%s%s %s" % (this_detail, status['operation'], whenstr) |
101 title="%s (queued)" % status['name'] |
115 |
102 elif status['type']=='current': |
116 detail=add_detail(detail, when_how_sched) |
|
117 |
|
118 elif status['state']==backup.State.QUEUED: |
|
119 detail=add_detail(detail, "queued") |
|
120 elif status['state']==backup.State.ACTIVE: |
103 # Operation running |
121 # Operation running |
104 progress='' |
122 progress='' |
105 if 'progress_current' in status and 'progress_total' in status: |
123 if 'progress_current' in status and 'progress_total' in status: |
106 progress=' %d%%' % (status['progress_current']/status['progress_total']) |
124 progress=' %d%%' % (status['progress_current']/status['progress_total']) |
107 elif 'original_size' in status and 'deduplicated_size' in status: |
125 elif 'original_size' in status and 'deduplicated_size' in status: |
108 progress=' %s→%s' % (humanbytes(status['original_size']), |
126 progress=' %s→%s' % (humanbytes(status['original_size']), |
109 humanbytes(status['deduplicated_size'])) |
127 humanbytes(status['deduplicated_size'])) |
110 title="%s (running: %s%s)" % (status['name'], status['operation'], progress) |
128 |
111 else: # status['type']=='nothing': |
129 howrunning = "running %s%s" % (status['operation'], progress) |
112 # Should be unscheduled, nothing running |
130 |
113 detail='' |
131 detail=add_detail(detail, howrunning) |
114 if state>=backup.BUSY and state in statestring: |
132 else: |
115 detail=' (' + statestring[state] + ')' |
133 pass |
116 title=status['name'] + detail |
134 |
117 |
135 if detail: |
118 return title, state |
136 title=status['name'] + ' (' + detail + ')' |
|
137 else: |
|
138 title=status['name'] |
|
139 |
|
140 return title, (status['state'], status['errors']) |
119 |
141 |
120 class BorgendTray(rumps.App): |
142 class BorgendTray(rumps.App): |
121 def __init__(self, backups): |
143 def __init__(self, backups): |
122 self.lock=Lock() |
144 self.lock=Lock() |
123 self.backups=backups |
145 self.backups=backups |
141 |
163 |
142 menu, state=self.__rebuild_menu() |
164 menu, state=self.__rebuild_menu() |
143 |
165 |
144 self.refresh_timer=None |
166 self.refresh_timer=None |
145 |
167 |
146 super().__init__(traynames[state], menu=menu, quit_button=None) |
168 super().__init__(trayname(state), menu=menu, quit_button=None) |
147 |
169 |
148 def __rebuild_menu(self): |
170 def __rebuild_menu(self): |
149 menu=[] |
171 menu=[] |
150 state=backup.INACTIVE |
172 state=(backup.State.INACTIVE, backup.Errors.OK) |
151 for index in range(len(self.backups)): |
173 for index in range(len(self.backups)): |
152 b=self.backups[index] |
174 b=self.backups[index] |
153 title, this_state=make_title(self.statuses[index]) |
175 title, this_state=make_title(self.statuses[index]) |
154 # Python closures suck dog's balls... |
176 # Python closures suck dog's balls... |
155 # first and the last program I write in Python until somebody |
177 # first and the last program I write in Python until somebody |
156 # fixes this brain damage |
178 # fixes this brain damage |
157 cbm=lambda sender, _b=b: self.__menu_select_backup(sender, _b) |
179 cbm=lambda sender, _b=b: self.__menu_select_backup(sender, _b) |
158 item=rumps.MenuItem(title, callback=cbm) |
180 item=rumps.MenuItem(title, callback=cbm) |
159 if this_state==backup.SCHEDULED: |
181 if not this_state[1].ok(): |
|
182 item.state=-1 |
|
183 elif this_state[0]==backup.State.SCHEDULED or this_state[0]==backup.State.QUEUED: |
160 item.state=1 |
184 item.state=1 |
161 elif this_state>=backup.BUSY: |
|
162 item.state=-1 |
|
163 menu.append(item) |
185 menu.append(item) |
164 state=backup.combine_state(state, this_state) |
186 state=combine_state(state, this_state) |
165 |
187 |
166 menu_log=rumps.MenuItem("Show log", callback=lambda _: showlog()) |
188 menu_log=rumps.MenuItem("Show log", callback=lambda _: showlog()) |
167 menu.append(menu_log) |
189 menu.append(menu_log) |
168 |
190 |
169 if not settings['no_quit_menu_entry']: |
191 if not settings['no_quit_menu_entry']: |
174 |
196 |
175 def refresh_ui(self): |
197 def refresh_ui(self): |
176 with self.lock: |
198 with self.lock: |
177 self.refresh_timer=None |
199 self.refresh_timer=None |
178 logger.debug('Rebuilding menu') |
200 logger.debug('Rebuilding menu') |
179 menu, active=self.__rebuild_menu() |
201 menu, state=self.__rebuild_menu() |
180 self.menu.clear() |
202 self.menu.clear() |
181 self.menu.update(menu) |
203 self.menu.update(menu) |
182 self.title=traynames[active] |
204 self.title=trayname(state) |
183 |
205 |
184 def __status_callback(self, index, status, errorlog=None): |
206 def __status_callback(self, index, status, errorlog=None): |
185 logger.debug('Status callback: %s' % str(status)) |
207 logger.debug('Status callback: %s' % str(status)) |
186 |
208 |
187 with self.lock: |
209 with self.lock: |