├── stiko.png ├── stiko-ok.png ├── stiko-notok.png ├── stiko-sync0.png ├── stiko-sync1.png ├── stiko-inactive.png ├── README.md └── stiko.py /stiko.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graboluk/stiko/HEAD/stiko.png -------------------------------------------------------------------------------- /stiko-ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graboluk/stiko/HEAD/stiko-ok.png -------------------------------------------------------------------------------- /stiko-notok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graboluk/stiko/HEAD/stiko-notok.png -------------------------------------------------------------------------------- /stiko-sync0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graboluk/stiko/HEAD/stiko-sync0.png -------------------------------------------------------------------------------- /stiko-sync1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graboluk/stiko/HEAD/stiko-sync1.png -------------------------------------------------------------------------------- /stiko-inactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graboluk/stiko/HEAD/stiko-inactive.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### **stiko** - systray icon for [syncthing](https://github.com/syncthing/syncthing) with server support 2 | 3 | --- 4 | 5 | ##### Installation and usage 6 | stiko is written in python 3 using gtk3 and tested on openbox with tint2 panel from debian jessie. It should run on a variety of other platforms. To install copy all the files to your folder of preference. In debian you will need the packages `python3`, `python3-requests`, `python3-gi` and `gir1.2-gtk-3.0`. 7 | 8 | To run it, execute `python3 stiko.py` (command line options are described below). An icon should appear in your systray. If you hover over it, useful info will appear in a tooltip - number of connected servers, current state (Up to Date, Uploading, Downloading, No Servers...), as well as download/upload progress: 9 | 10 | ![Example screenshot](/../screenshots/screenshots/1.png?raw=true) 11 | ![Example screenshot](/../screenshots/screenshots/2.png?raw=true) 12 | ![Example screenshot](/../screenshots/screenshots/4.png?raw=true) 13 | ![Example screenshot](/../screenshots/screenshots/6.png?raw=true) 14 | 15 | Even more info is accessible via right-click menu: 16 | 17 | ![Example screenshot](/../screenshots/screenshots/menu1.png?raw=true) 18 | ![Example screenshot](/../screenshots/screenshots/menu2.png?raw=true) 19 | 20 | Left-clicking will open a new brwoser tab with syncthing web gui. 21 | 22 | ##### Command line options 23 | 24 | ||| 25 | |---|---| 26 | | `--servers server1 server2 ...`| Space separated list of devices which should be treated as servers (defauts to all connected devices). Stiko will report "Up to Date" only if the local files are up to date and at least one of the servers is up to date | 27 | | `--icons ICON_FOLDER`| Folder containing the icons. Defaults to the directory containing `stiko.py`| 28 | | `--sturl SYNCTHING_URL`| Complete URL of a syncthing instance. Defaults to `http://localhost:8384`| 29 | | `--stfolder FOLDER_NAME`| Name of the sycthing folder to monitor. Defaults to `default`. Currently stiko works correctly only if one folder is present| 30 | 31 | 32 | ##### Example 33 | Suppose you have a cheap VPS (syncthing device name CHEAPO). The price is great but uptime is at 95%. But you also have a computer at work (syncthing device name OFFICEMATE) which you keep on almost all the time. Together their uptime is pretty much 100%. It would be nice to use both of them in combination as a server to/from which you syncronize all your other devices, for example your laptop. 34 | 35 | Stiko allows you to use syncthing this way. Just setup syncthing on all your devices (including CHEAPO and OFFICEMATE) in the standard way. Now on your laptop run `stiko.py --servers CHEAPO OFFICEMATE`. Stiko will inform you if at least one of the devices CHEAPO and OFFICEMATE are in sync with your laptop. 36 | 37 | ##### Meaning of icons 38 | 1. If local files are Up to Date, and **at least one server** is Up to Date, then the icon is blue. 39 | 2. If no servers are connected then the icon is grey. 40 | 3. If syncthing instance can't be contacted then the icon is red. 41 | 4. the icon wiggles if either local files are not up to date or none of the servers are up to date. Hover the cursor over the icon to find out the details. 42 | 43 | ##### License 44 | Licensed under GPL3. But in case you would like to contribute I require all contribution to be dual licensed MPL2/GPL3, so that it's easy to change the license to another free software license if ever need be. 45 | -------------------------------------------------------------------------------- /stiko.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3.4 2 | 3 | import time 4 | import requests 5 | import sys 6 | import os 7 | import argparse 8 | import datetime 9 | 10 | from gi import require_version 11 | require_version("Gtk", "3.0") 12 | from gi.repository import Gtk, GObject, GdkPixbuf 13 | 14 | import threading 15 | import collections 16 | import webbrowser 17 | 18 | # pango markup 19 | black = '' 20 | green = '' 21 | gray = '' 22 | blue = '' 23 | red = '' 24 | span = '' 25 | sgray = '' 26 | 27 | 28 | 29 | class STDetective(threading.Thread): 30 | def __init__(self, gui, servers): 31 | super(STDetective, self).__init__() 32 | self.gui = gui 33 | 34 | #flag for terminating this thread when icon terminates 35 | self.isOver = False 36 | 37 | self.server_names = servers 38 | self.server_ids = [] 39 | self.connected_ids = [] 40 | self.connected_server_ids = [] 41 | 42 | # server_completion really lists all peers. 43 | # We only get cached values from st so it's 44 | # not expensive to query for all of them 45 | self.server_completion = {} 46 | 47 | 48 | # p as a prefix to a variable name means 49 | # that this is the previously checked value 50 | self.connections = {} 51 | self.pconnections = {} 52 | 53 | #dictionary for translating device ids to syncthing names 54 | self.id_dict = {} 55 | self.myID = 0 56 | 57 | # current and previous inSyncFiles, globalFiles, inSyncBytes, globalBytes 58 | self.a,self.b,self.c,self.d, self.pa,self.pb,self.pc,self.pd = [0,0,0,0,0,0,0,0] 59 | 60 | self.isDownloading = False 61 | self.isUploading = False 62 | self.isSTAvailable = False 63 | 64 | self.DlCheckTime = datetime.datetime.today() 65 | self.pDlCheckTime = self.DlCheckTime 66 | self.local_index_stamp = self.DlCheckTime 67 | 68 | self.UlSpeeds = collections.deque(maxlen=2) 69 | self.DlSpeeds = collections.deque(maxlen=2) 70 | 71 | self.QuickestServerID='' 72 | self.config = {} 73 | 74 | self.peer_ulspeeds = {} 75 | self.peer_dlspeeds = {} 76 | self.peer_completion = {} 77 | 78 | def basic_init(self): 79 | # we add basic_init() (instead of calling everything from __init__()) 80 | # because otherwise if the basic checks below fail gui is unresponsive 81 | # (because main_loop wouldn't be working yet). This will be ran from self.run() 82 | 83 | self.config = self.request_config() 84 | self.myID = self.request_myid() 85 | 86 | # A thread-safe way to demand gui updates. We do it here 87 | # because request_config() hopefully changed isSTAvailable to True 88 | self.update_gui() 89 | 90 | while not self.config: 91 | time.sleep(3) 92 | config = self.request_config() 93 | self.update_gui() 94 | if self.isOver: sys.exit() 95 | 96 | for a in self.config["devices"]: 97 | self.id_dict[a["deviceID"]] = a['name'] 98 | 99 | if any([not (a in self.id_dict.values()) for a in self.server_names]): 100 | print("Some provided server names are wrong.") 101 | Gtk.main_quit() 102 | sys.exit() 103 | 104 | if any([not (a in self.id_dict.keys()) for a in self.server_ids]): 105 | print("Some provided server ids are wrong.") 106 | Gtk.main_quit() 107 | sys.exit() 108 | 109 | if all([not f["id"] == STFolder for f in self.config["folders"]]): 110 | print("No such folder reported by syncthing") 111 | Gtk.main_quit() 112 | sys.exit() 113 | 114 | if not self.server_names and not self.server_ids: 115 | self.server_ids = self.id_dict.keys() 116 | else: 117 | self.server_ids = [a for a in self.id_dict.keys() if (self.id_dict[a] in self.server_names or a in self.server_ids)] 118 | 119 | def update_gui(self): 120 | GObject.idle_add(lambda :self.gui.update_icon(self)) 121 | GObject.idle_add(lambda :self.gui.menu.update_menu(self)) 122 | GObject.idle_add(lambda :self.gui.peer_menu.update_menu(self)) 123 | 124 | def DlCheck(self): 125 | #~ print("DLCheck()") 126 | #if (datetime.datetime.today() -self.pDlCheckTime).total_seconds() <3: return 127 | 128 | self.pa, self.pb,self.pc, self.pd = self.a,self.b,self.c,self.d 129 | self.a,self.b,self.c,self.d= self.request_local_completion() 130 | self.pDlCheckTime = self.DlCheckTime 131 | self.DlCheckTime = datetime.datetime.today() 132 | self.update_dl_state() 133 | self.DlSpeeds.append((self.c-self.pc)/(self.DlCheckTime-self.pDlCheckTime).total_seconds()) 134 | 135 | def UlCheck(self): 136 | #~ print("ULCheck()") 137 | 138 | # this is a dirty hack - we give ourselves 7 seconds of hope 139 | # that all servers will report their FolderCompletions less than 100. Otherwise 140 | # the icon will go "OK" and only after FolderCompletions arrive will it go to "Sync" again 141 | if (datetime.datetime.today() - self.local_index_stamp).total_seconds() >7: 142 | self.update_ul_state() 143 | 144 | 145 | def update_connection_data(self): 146 | self.pconnections = self.connections 147 | self.connections = self.request_connections() 148 | 149 | self.connected_ids = [ a for a in list(self.connections.keys()) if self.connections[a]["connected"]] 150 | self.connected_server_ids = [s for s in self.server_ids if s in self.connected_ids] 151 | 152 | self.request_server_completion() 153 | 154 | for a in self.pconnections.keys(): 155 | if a in self.connections.keys(): 156 | if not a in self.peer_ulspeeds.keys(): self.peer_ulspeeds[a] = collections.deque(maxlen=2) 157 | if not a in self.peer_dlspeeds.keys(): self.peer_dlspeeds[a] = collections.deque(maxlen=2) 158 | out_byte_delta = self.connections[a]["outBytesTotal"] - self.pconnections[a]["outBytesTotal"] 159 | in_byte_delta = self.connections[a]["inBytesTotal"] - self.pconnections[a]["inBytesTotal"] 160 | 161 | try: 162 | time = datetime.datetime.strptime(self.connections[a]["at"][:24], '%Y-%m-%dT%H:%M:%S.%f') 163 | ptime = datetime.datetime.strptime(self.pconnections[a]["at"][:24], '%Y-%m-%dT%H:%M:%S.%f') 164 | except: 165 | #occasionally syncthing does not report the %f part 166 | time = datetime.datetime.strptime(self.connections[a]["at"][:19], '%Y-%m-%dT%H:%M:%S') 167 | ptime = datetime.datetime.strptime(self.pconnections[a]["at"][:19], '%Y-%m-%dT%H:%M:%S') 168 | 169 | if not time == ptime: 170 | self.peer_ulspeeds[a].append(out_byte_delta/(time-ptime).total_seconds()) 171 | self.peer_dlspeeds[a].append(in_byte_delta/(time-ptime).total_seconds()) 172 | 173 | def request_config(self): 174 | if self.isOver: sys.exit() 175 | try: 176 | c = requests.get(STUrl+'/rest/system/config').json() 177 | self.isSTAvailable = True 178 | return c 179 | except: 180 | #~ raise 181 | self.isSTAvailable = False 182 | return False 183 | 184 | def request_myid(self): 185 | if self.isOver: sys.exit() 186 | try: 187 | c = requests.get(STUrl+'/rest/system/status').json() 188 | self.isSTAvailable = True 189 | return c["myID"] 190 | except: 191 | self.isSTAvailable = False 192 | return False 193 | 194 | def request_connections(self): 195 | if self.isOver: sys.exit() 196 | try: 197 | connections = requests.get(STUrl+'/rest/system/connections').json()["connections"] 198 | self.isSTAvailable = True 199 | return connections 200 | except: 201 | #~ raise 202 | self.isSTAvailable = False 203 | return {} 204 | 205 | def request_local_completion(self): 206 | if self.isOver: sys.exit() 207 | try: 208 | c = requests.get(STUrl+'/rest/db/status?folder='+STFolder) 209 | self.isSTAvailable = True 210 | return c.json()["inSyncFiles"], c.json()["globalFiles"], c.json()["inSyncBytes"], c.json()["globalBytes"] 211 | except: 212 | #~ raise 213 | self.isSTAvailable = False 214 | return self.a,self.b,self.c,self.d 215 | 216 | def request_remote_completion(self,devid): 217 | if self.isOver: sys.exit() 218 | try: 219 | c = requests.get(STUrl+'/rest/db/completion?device='+devid+'&folder='+STFolder) 220 | self.isSTAvailable = True 221 | return c.json()["completion"] 222 | except: 223 | self.isSTAvailable = False 224 | return 0 225 | 226 | def request_server_completion(self): 227 | for s in self.connected_ids: 228 | self.server_completion[s] = self.request_remote_completion(s) 229 | 230 | def request_events(self,since, Timeout): 231 | #~ print("request_events() "+str(Timeout)) 232 | if self.isOver: sys.exit() 233 | try: 234 | events = requests.get(STUrl+'/rest/events?since='+str(since), timeout=Timeout).json() 235 | self.isSTAvailable = True 236 | return events 237 | except: 238 | # there seems to be a bug in requests. As a workaround 239 | # we will just ignore error when Timeout is 2. If syncthing really is not 240 | # accessible then it will be caught soon (in request_connections for example) 241 | if Timeout >3: 242 | self.isSTAvailable = False 243 | return [] 244 | 245 | def update_ul_state(self): 246 | 247 | # this seems to be the only place where server_completion 248 | # should really mean that we look only at the servers, 249 | # so s below is server_completion restricted to servers 250 | s = {} 251 | for a in self.server_completion.keys(): 252 | if a in self.connected_server_ids: s[a] = self.server_completion[a] 253 | #print(s) 254 | 255 | if s and all((not p == 100) for p in s.values()): 256 | self.isUploading = True 257 | try: 258 | self.QuickestServerID =max(s.keys(), key = lambda x: s[x]) 259 | except: 260 | self.QuickestServerID='' 261 | else: 262 | self.isUploading = False 263 | self.QuickestServerID='' 264 | 265 | def update_dl_state(self): 266 | if not self.a == self.b or not self.c == self.d: 267 | self.isDownloading = True 268 | else: 269 | self.isDownloading = False 270 | 271 | def run(self): 272 | #~ print("run()") 273 | 274 | self.basic_init() 275 | next_event=1 276 | 277 | self.a,self.b,self.c,self.d = self.request_local_completion() 278 | self.update_gui() 279 | 280 | while not self.isOver: 281 | self.update_connection_data() 282 | #we do basic_init because perhaps new peers appeared 283 | self.basic_init() 284 | 285 | self.DlCheck() 286 | self.UlCheck() 287 | self.update_gui() 288 | 289 | # the above calls should give correct answers as to whether 290 | # we are uploading, etc. We use also the event loop, in order to 291 | # 1) react to things quicker, 2) to know that something is happening 292 | # so that we have to run the calls above (request_events() is blocking) 293 | 294 | be_quick = self.isDownloading or self.isUploading or any([ not t.server_completion[a] ==100 for a in self.connected_ids]) 295 | events = self.request_events(next_event, 2 if be_quick else 65) 296 | for v in events: 297 | #~ print(v["type"]+str(v["id"])) 298 | 299 | # The "stamp" is heuristic, we are giving ourselves better chances 300 | # to report events picked-up in the event loop 301 | #~ if v["type"] == "StateChanged" and v["data"]["to"] == "scanning": 302 | #~ self.isUploading = True 303 | #~ self.local_index_stamp = datetime.datetime.today() 304 | if v["type"] == "LocalIndexUpdated" and self.connected_server_ids: 305 | self.isUploading = True 306 | self.local_index_stamp = datetime.datetime.today() 307 | 308 | elif v["type"] == "RemoteIndexUpdated": 309 | self.isDownloading = True 310 | 311 | elif str(v["type"]) == "FolderSummary": 312 | w = v["data"]["summary"] 313 | self.pa, self.pb,self.pc, self.pd = self.a,self.b,self.c,self.d 314 | self.a,self.b,self.c,self.d = w["inSyncFiles"], w["globalFiles"], w["inSyncBytes"], w["globalBytes"] 315 | self.pDlCheckTime = self.DlCheckTime 316 | self.DlCheckTime = datetime.datetime.today() 317 | self.DlSpeeds.append((self.c-self.pc)/(self.DlCheckTime-self.pDlCheckTime).total_seconds()) 318 | self.update_dl_state() 319 | 320 | elif v["type"] == "FolderCompletion": 321 | if v["data"]["device"] in self.connected_server_ids: 322 | self.server_completion[v["data"]["device"]] = v["data"]["completion"] 323 | self.update_ul_state() 324 | 325 | self.update_gui() 326 | if events: next_event = events[len(events)-1]["id"] 327 | 328 | sys.exit() 329 | 330 | 331 | class PeerMenu(Gtk.Menu): 332 | def __init__ (self,gui): 333 | super(PeerMenu,self).__init__() 334 | self.set_reserve_toggle_size(False) 335 | 336 | self.gui = gui 337 | self.is_visible = False 338 | self.peer_info = Gtk.MenuItem('') 339 | info_str = gray+ " "*25+span 340 | self.peer_info.get_children()[0].set_markup(info_str) 341 | self.peer_info.set_sensitive(False) 342 | 343 | self.append(self.peer_info) 344 | self.peer_info.show() 345 | 346 | def update_menu(self, t): 347 | all_str = gray + 'name status UL / DL'+span+sgray+' (KB/s)' +span 348 | info_str = '' 349 | for a in t.connected_ids: 350 | #print(a) 351 | #print(t.myID) 352 | if a == t.myID: continue 353 | info_str = '\n' 354 | info_str += black + t.id_dict[a][:10] + span 355 | try: 356 | if t.server_completion[a] == 100: 357 | miss ='OK' 358 | info_str += green + ' '*(4+ 10-len(t.id_dict[a]))+ miss + span 359 | else: 360 | miss = str(round((t.d-t.server_completion[a]*t.d/100)/1000000,2))+'MB' 361 | info_str += blue +' '*(4+ 10-len(t.id_dict[a])) +miss+span 362 | ustr = ('%.0f' % max(0,sum(list(t.peer_ulspeeds[a]))/5000)) 363 | info_str += black +' '*(10-len(miss))+ ' '*(6-len(ustr))+ ustr + ' / ' 364 | info_str += ('%.0f' % max(0,sum(list(t.peer_dlspeeds[a]))/5000))+span 365 | except: pass 366 | all_str +=info_str 367 | 368 | self.peer_info.get_children()[0].set_markup(all_str) 369 | 370 | class StikoMenu(Gtk.Menu): 371 | def __init__ (self,gui): 372 | super(StikoMenu,self).__init__() 373 | self.gui = gui 374 | self.is_visible = False 375 | 376 | self.server_item = Gtk.MenuItem('\n\n\n\n\n\n\n\n\n\n\n') 377 | self.sep = Gtk.SeparatorMenuItem() 378 | self.progress_item = Gtk.MenuItem('') 379 | self.sep2 = Gtk.SeparatorMenuItem() 380 | self.all_peers_item = Gtk.MenuItem('') 381 | self.close_item = Gtk.MenuItem('') 382 | 383 | self.append(self.server_item) 384 | self.append(self.sep) 385 | self.append(self.progress_item) 386 | self.append(self.sep2) 387 | self.append(self.all_peers_item) 388 | self.append(self.close_item) 389 | 390 | self.all_peers_item.set_submenu(gui.peer_menu) 391 | self.all_peers_item.connect_object("select", self.select_peer_menu_callback,None) 392 | self.all_peers_item.connect_object("deselect", self.deselect_peer_menu_callback,None) 393 | 394 | self.close_item.connect_object("activate", lambda x: Gtk.main_quit(),None) 395 | 396 | self.server_item.show() 397 | self.sep.show() 398 | self.progress_item.show() 399 | self.sep2.show() 400 | self.all_peers_item.show() 401 | self.close_item.show() 402 | 403 | self.connect("deactivate", self.deactivate_callback) 404 | self.set_reserve_toggle_size(False) 405 | 406 | self.close_item.get_children()[0].set_markup(black+"Close stiko"+span) 407 | self.all_peers_item.get_children()[0].set_markup(black+"Peer info"+span) 408 | 409 | def select_peer_menu_callback(self,x): 410 | self.gui.peer_menu.is_visible = True 411 | 412 | def deselect_peer_menu_callback(self,x): 413 | self.gui.peer_menu.is_visible = False 414 | 415 | def deactivate_callback(self, menu): 416 | self.is_visible = False 417 | 418 | def update_menu(self, t): 419 | self.updater(t) 420 | #if self.is_visible: GObject.timeout_add(1000, self.updater,t) 421 | 422 | def updater(self,t): 423 | if not t.isSTAvailable: 424 | info_str = red+"No contact with syncthing"+span 425 | 426 | elif not t.connected_server_ids: 427 | info_str = gray+"No servers"+span 428 | 429 | else: 430 | info_str =gray+ "Connected Servers ("+str(len(t.connected_server_ids))+'/'+str(len(t.server_ids))+')'+span 431 | for a in t.connected_server_ids: 432 | info_str += black+ '\n '+t.id_dict[a][:10] +span 433 | if a in t.server_completion.keys() and t.server_completion[a] == 100: 434 | info_str += green + ' '*(6+ 10-len(t.id_dict[a]))+ 'OK'+ span 435 | elif a in t.server_completion.keys(): 436 | info_str += blue +' '*(4+ 10-len(t.id_dict[a])) +str(round((t.d-t.server_completion[a]*t.d/100)/1000000,2))+'MB'+span 437 | else: 438 | info_str += blue +' '*(4+ 10-len(t.id_dict[a])) +"..."+span 439 | 440 | 441 | # Apparently this is te only way of accessing the label of a GTk.MenuItem 442 | self.server_item.get_children()[0].set_markup(info_str) 443 | 444 | self.server_item.set_sensitive(False) 445 | 446 | info_str =gray+ "Local Status"+span 447 | if t.isDownloading: 448 | if not t.a==t.b: 449 | info_str += blue +' '*3+ str(round((t.d-t.c)/1000000,2)) + 'MB'+span 450 | info_str += black+ '\n('+str(t.b-t.a)+" file" +('s' if t.b-t.a>1 else '')+span 451 | #info_str += black + str(round((t.d-t.c)/1000000,2))+'MB @ '+span 452 | info_str +=black + ' @ '+ ('%.0f' % max(0,sum(list(t.DlSpeeds))/5000)) +'KB/s)'+span 453 | else: 454 | info_str += black +"\nChecking indices..."+span 455 | 456 | if t.isUploading: 457 | if not t.isDownloading: info_str +=black+'\n'+span 458 | if t.QuickestServerID: 459 | info_str += blue + "\nUL to "+t.id_dict[t.QuickestServerID] +span 460 | info_str += black +'\n('+str(round((t.d-t.server_completion[t.QuickestServerID]*t.d/100)/1000000,2))+'MB' 461 | try: 462 | info_str += ' @ '+ ('%.0f' % max(0,sum(list(t.peer_ulspeeds[t.QuickestServerID]))/5000)) +'KB/s)' +span 463 | except: 464 | info_str += ')'+span 465 | else: 466 | info_str += blue+"\nUploading... \n "+span 467 | 468 | if t.isSTAvailable and len(t.connected_server_ids) and not t.isDownloading and not t.isUploading: 469 | info_str += green+' '*5+"OK\n\n\n"+span 470 | info_str += '\n'*(3-info_str.count('\n')) 471 | 472 | self.progress_item.get_children()[0].set_markup(info_str) 473 | self.progress_item.set_sensitive(False) 474 | 475 | 476 | 477 | return self.is_visible 478 | 479 | 480 | class StikoGui(Gtk.StatusIcon): 481 | def __init__ (self, iconDir): 482 | super(StikoGui, self).__init__() 483 | 484 | try: 485 | self.px_good = GdkPixbuf.Pixbuf.new_from_file(os.path.join(iconDir,'stiko-ok.png')) 486 | self.px_noST = GdkPixbuf.Pixbuf.new_from_file(os.path.join(iconDir,'stiko-notok.png')) 487 | self.px_noServer = GdkPixbuf.Pixbuf.new_from_file(os.path.join(iconDir,'stiko-inactive.png')) 488 | self.px_sync = [GdkPixbuf.Pixbuf.new_from_file(os.path.join(iconDir,'stiko-sync1.png')), 489 | GdkPixbuf.Pixbuf.new_from_file(os.path.join(iconDir,'stiko-sync0.png'))] 490 | except: 491 | #~ raise 492 | print("I coudn't open icon files.") 493 | sys.exit() 494 | 495 | self.set_from_pixbuf(self.px_noServer) 496 | self.connect('activate', self.on_left_click) 497 | self.connect('popup-menu', self.on_right_click) 498 | 499 | self.animation_counter = 1 500 | self.isAnimated = False #for controlling animation only 501 | 502 | self.peer_menu = PeerMenu(self) 503 | self.menu = StikoMenu(self) 504 | 505 | def on_left_click(self,icon): 506 | #icon.set_visible(False) 507 | webbrowser.open_new_tab(STUrl) 508 | 509 | def on_right_click(self, data, event_button, event_time): 510 | self.menu.popup(None,None,None,None,event_button,event_time) 511 | self.menu.is_visible = True 512 | 513 | def update_icon(self,t): 514 | #print([t.isSTAvailable, len(t.connected_server_ids), t.isDownloading, t.isUploading]) 515 | 516 | info_str = '' 517 | if not t.isSTAvailable: 518 | info_str += "No contact with syncthing" 519 | self.set_from_pixbuf(self.px_noST) 520 | self.isAnimated=False 521 | elif not t.connected_server_ids: 522 | info_str += "No servers" 523 | self.set_from_pixbuf(self.px_noServer) 524 | self.isAnimated=False 525 | 526 | elif t.isDownloading or t.isUploading: 527 | info_str += str(len(t.connected_server_ids))+" Server" +('s' if len(t.connected_server_ids) >1 else '') 528 | if t.isDownloading: 529 | if not t.a==t.b: 530 | info_str += "\nDownloading "+str(t.b-t.a)+" file" +('s' if t.b-t.a>1 else '') 531 | info_str += ' ('+str(round((t.d-t.c)/1000000,2))+'MB @ ' 532 | info_str += ('%.0f' % max(0,sum(list(t.DlSpeeds))/5000)) +'KB/s)' 533 | else: 534 | info_str += "\nChecking indices" 535 | 536 | if t.isUploading: 537 | if t.QuickestServerID: 538 | info_str += "\nUploading to "+t.id_dict[t.QuickestServerID] 539 | info_str += ' ('+str(round((t.d-t.server_completion[t.QuickestServerID]*t.d/100)/1000000,2))+'MB' 540 | try: 541 | info_str += ' @ '+ ('%.0f' % max(0,sum(list(t.peer_ulspeeds[t.QuickestServerID]))/5000)) +'KB/s)' 542 | except: 543 | info_str += ')' 544 | else: 545 | info_str += "\nUploading..." 546 | 547 | if not self.isAnimated: 548 | self.isAnimated = True 549 | self.set_from_pixbuf(self.px_sync[0]) 550 | self.animation_counter = 1 551 | GObject.timeout_add(600, self.update_icon_animate,t) 552 | else: 553 | info_str += str(len(t.connected_server_ids))+" Server" +('s' if len(t.connected_server_ids) >1 else '')+"\nUp to Date" 554 | self.set_from_pixbuf(self.px_good) 555 | self.isAnimated=False 556 | 557 | self.set_tooltip_text(info_str) 558 | while Gtk.events_pending(): Gtk.main_iteration_do(True) 559 | 560 | def update_icon_animate(self,t): 561 | #~ print("update icon animate") 562 | #~ print ([t.isDownloading, t.isUploading,t.isSTAvailable, len(t.connected_server_ids), self.isAnimated]) 563 | if (t.isDownloading or t.isUploading) and t.isSTAvailable and t.connected_server_ids and self.isAnimated: 564 | self.set_from_pixbuf(self.px_sync[self.animation_counter]) 565 | self.animation_counter = (self.animation_counter + 1) % 2 566 | return True 567 | else: 568 | return False 569 | 570 | 571 | 572 | parser = argparse.ArgumentParser(description = 'This is stiko, a systray icon for syncthing.',epilog='', usage='stiko.py [options]') 573 | parser.add_argument('--servers', nargs = '+', default ='',help = 'List of names of devices treated as servers, space separated. If empty then all connected devices will be treated as servers.',metavar='') 574 | parser.add_argument('--icons', default ='', help = 'Path to the directory with icons. Defaults to this script\'s directory ('+os.path.dirname(os.path.abspath(__file__))+')', action="store", metavar='') 575 | parser.add_argument('--sturl', default ='', help = 'URL of a syncthing instance. Defaults to "http://localhost:8384"', action="store", metavar='') 576 | parser.add_argument('--stfolder', default ='', help = 'Name of the syncthing folder to monitor. Defaults to "default"', action="store", metavar='') 577 | 578 | args = parser.parse_args(sys.argv[1:]) 579 | iconDir = os.path.dirname(__file__) if not args.icons else args.icons 580 | STUrl = "http://localhost:8384" if not args.sturl else args.sturl 581 | STFolder = 'default' if not args.stfolder else args.stfolder 582 | 583 | GObject.threads_init() 584 | 585 | gui = StikoGui(iconDir) 586 | 587 | t = STDetective(gui,args.servers) 588 | 589 | 590 | # we make t a daemon because http requests are 591 | # blocking, so otherwise we hae to wait for 592 | # termination up to 60s (or whatever the syncthing 593 | # ping interval is) 594 | t.daemon = True 595 | 596 | t.start() 597 | 598 | Gtk.main() 599 | t.isOver = True 600 | --------------------------------------------------------------------------------