| 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: |