├── .gitignore ├── LICENSE ├── README.md ├── debian ├── changelog ├── compat ├── control ├── copyright ├── install ├── rules └── source │ └── format ├── udisks-indicator └── udisks-indicator.desktop /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Sergiy Kolodyazhnyy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # udisks-indicator 2 | Indicator for Ubuntu with Unity desktop to show disk usage 3 | 4 | *With "Adwaita" Icon Theme* 5 | 6 | [![Adwaita](http://i.imgur.com/Nc911nP.png)](http://i.imgur.com/Nc911nP.png) 7 | 8 | *With Ubuntu Kylin Icon Theme* 9 | 10 | [![UbuntuKylin](http://i.imgur.com/mgZ2Wb8.png)](http://i.imgur.com/mgZ2Wb8.png) 11 | 12 | ### Overview 13 | 14 | This indicator for Ubuntu with Unity allows easily view information about your mounted partitions. 15 | 16 | Entries are organized in order: 17 | 18 | - Partition 19 | - Alias ( if set by user ) 20 | - Disk Drive to which partition belongs 21 | - Mountpoint of the partition ( directory ) 22 | - Filesystem type 23 | - Usage in percent and human readable format 24 | - Usage bar 25 | 26 | Preferences dialog allows removing Alias,Disk,Mountpoint,and Filesystem fields. Since the original purpose of this indicator was to display partition usage information, Partition , Usageand Usage Bar are kept and non-removable 27 | 28 | Clicking on each partition entry will open the partition's mountpoint in the default file manager 29 | 30 | The "Unmounted Partitions" menu lists all the partitions not currently mounted by the system. Clicking on any entry in that submenu will mount that partition automatically, typically to `/media/username/drive-id` folder, unless your system has appropriate entry in `/etc/fstab` file to mount it elsewhere. 31 | 32 | The indicator uses default icons provided with the system, so the icon should be changing as you change the icon theme using Unity Tweak Tool or other methods. Information fields for each entry also adapt to nice formating if you are using Ubuntu's default font or any Monospace font ( others aren't supported) 33 | 34 | In addition, the indicator provides bubble notifications if there are mounting errors 35 | 36 | ### Featured articles: 37 | 38 | - [OMG!Ubuntu! Nifty Partition Mount Indicator for Ubuntu Gets Even Niftier](http://www.omgubuntu.co.uk/2016/10/udisks-indicator-partition-mount-applet-ubuntu) (features v2.0) 39 | - [OMG!Ubuntu! http://www.omgubuntu.co.uk/2016/09/udisks-indicator-applet-istat-menus-linux](http://www.omgubuntu.co.uk/2016/09/udisks-indicator-applet-istat-menus-linux) (features v1.0) 40 | 41 | 42 | 43 | [![paypalbutton](https://www.paypal.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=CB9L72S9LEF66) 44 | 45 | 46 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | udisks-indicator (0.1-1) unstable; urgency=low 2 | 3 | * Initial release 4 | 5 | -- Nathan Osman Mon, 03 Oct 2016 22:40:26 -0700 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: udisks-indicator 2 | Section: gnome 3 | Priority: extra 4 | Maintainer: Nathan Osman 5 | Build-Depends: debhelper (>= 9) 6 | Standards-Version: 3.9.8 7 | Homepage: https://github.com/SergKolo/udisks-indicator 8 | 9 | Package: udisks-indicator 10 | Architecture: all 11 | Depends: python3-gi, 12 | gir1.2-appindicator3-0.1, 13 | ${python:Depends}, 14 | ${misc:Depends} 15 | Description: Disk usage indicator 16 | Indicator for viewing information about mounted partitions. It strives to be 17 | visually similar to iStat Menu 3 for OS X. 18 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: udisks-indicator 3 | Source: https://github.com/SergKolo/udisks-indicator 4 | 5 | Files: * 6 | Copyright: Copyright (c) 2016 Sergiy Kolodyazhnyy 7 | License: MIT 8 | 9 | Files: debian/* 10 | Copyright: 2016 Nathan Osman 11 | License: MIT 12 | 13 | License: MIT 14 | Permission is hereby granted, free of charge, to any person obtaining a copy 15 | of this software and associated documentation files (the "Software"), to deal 16 | in the Software without restriction, including without limitation the rights 17 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 18 | copies of the Software, and to permit persons to whom the Software is 19 | furnished to do so, subject to the following conditions: 20 | . 21 | The above copyright notice and this permission notice shall be included in 22 | all copies or substantial portions of the Software. 23 | . 24 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 27 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 28 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 29 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 30 | THE SOFTWARE. 31 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | udisks-indicator usr/bin/ 2 | udisks-indicator.desktop usr/share/applications/ 3 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /udisks-indicator: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # 5 | # Author: Serg Kolo , contact: 1047481448@qq.com 6 | # Date: September 27 , 2016 7 | # Purpose: appindicator for displaying mounted filesystem usage 8 | # Tested on: Ubuntu 16.04 LTS 9 | # 10 | # 11 | # Licensed under The MIT License (MIT). 12 | # See included LICENSE file or the notice below. 13 | # 14 | # Copyright © 2016 Sergiy Kolodyazhnyy 15 | # 16 | # Permission is hereby granted, free of charge, to any person obtaining a copy 17 | # of this software and associated documentation files (the "Software"), to deal 18 | # in the Software without restriction, including without limitation the rights 19 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | # copies of the Software, and to permit persons to whom the Software is 21 | # furnished to do so, subject to the following conditions: 22 | # 23 | # The above copyright notice and this permission notice shall be included 24 | # in all copies or substantial portions of the Software. 25 | # 26 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 32 | # SOFTWARE. 33 | import gi 34 | gi.require_version('AppIndicator3', '0.1') 35 | gi.require_version('Notify', '0.7') 36 | from gi.repository import GLib as glib 37 | from gi.repository import AppIndicator3 as appindicator 38 | from gi.repository import Gtk as gtk 39 | from gi.repository import Gio 40 | from gi.repository import Notify 41 | from os import statvfs 42 | # from collections import OrderedDict 43 | import subprocess 44 | import copy 45 | import shutil 46 | import dbus 47 | import math 48 | import json 49 | import os 50 | 51 | 52 | class UdisksIndicator(object): 53 | 54 | def __init__(self): 55 | self.app = appindicator.Indicator.new( 56 | 'udisks-indicator', "drive-harddisk-symbolic.svg", 57 | appindicator.IndicatorCategory.HARDWARE 58 | ) 59 | 60 | if self.is_mate(): 61 | self.app.set_icon("drive-harddisk-symbolic") 62 | 63 | self.app.set_status(appindicator.IndicatorStatus.ACTIVE) 64 | 65 | self.fields = ['Partition', 'Alias', 'Drive', 66 | 'MountPoint', 'Filesystem', 'Usage' 67 | ] 68 | 69 | self.user_home = os.path.expanduser('~') 70 | filename = '.udisks-indicator-preferences.json' 71 | self.prefs_file = os.path.join(self.user_home, filename) 72 | self.prefs = self.read_prefs_file() 73 | 74 | filename = '.partition_aliases.json' 75 | self.config_file = os.path.join(self.user_home, filename) 76 | self.cache = self.get_partitions() 77 | 78 | self.note = Notify.Notification.new(__file__, None, None) 79 | Notify.init(__file__) 80 | 81 | self.cache_icon_theme = self.gsettings_get( 82 | 'org.gnome.desktop.interface', None, 'icon-theme' 83 | ) 84 | 85 | self.mounted_devs = {} 86 | 87 | self.make_menu() 88 | self.update() 89 | 90 | def is_mate(self): 91 | """ detect mate session """ 92 | panel_pid = self.run_cmd(['pgrep', 'mate-panel']) 93 | if panel_pid: 94 | return True 95 | 96 | def read_prefs_file(self, *args): 97 | default = {'fields': [True] * 5, 'autostart': False} 98 | if not os.path.exists(self.prefs_file): 99 | return default 100 | 101 | with open(self.prefs_file) as f: 102 | try: 103 | return json.load(f) 104 | except: 105 | return default 106 | 107 | def write_prefs_file(self, *args): 108 | with open(self.prefs_file, 'w') as f: 109 | try: 110 | json.dump(self.prefs, f, 111 | indent=4, sort_keys=True 112 | ) 113 | except Exception as e: 114 | self.send_notif( 115 | self.note, 116 | 'Failed writing ' + self.prefs_file, 117 | str(e) 118 | ) 119 | 120 | def update(self): 121 | timeout = 15 122 | glib.timeout_add_seconds(timeout, self.callback) 123 | 124 | def callback(self): 125 | current = self.get_partitions() 126 | icon_theme = self.gsettings_get( 127 | 'org.gnome.desktop.interface', None, 'icon-theme' 128 | ) 129 | if self.cache != current: 130 | # print('cache changed') 131 | if len(self.cache) != len(current): 132 | # print('rebuilding menu') 133 | self.make_menu() 134 | else: 135 | for i in current: 136 | # print('updating label') 137 | # print(self.make_label(i)) 138 | label = '\n'.join([j for j in self.make_label(i) if j]) 139 | self.update_label(i[0], label) 140 | self.cache = current 141 | if self.cache_icon_theme != icon_theme: 142 | self.cache_icon_theme = icon_theme 143 | self.make_menu() 144 | self.update() 145 | 146 | def update_label(self, partition, label): 147 | key = [k for k in self.mounted_devs.keys() if partition in k] 148 | self.mounted_devs[key[0]].set_label(label) 149 | 150 | def add_menu_item(self, menu_obj, item_type, image, label, action, args): 151 | """ dynamic function that can add menu items depending on 152 | the item type and other arguments""" 153 | menu_item, icon = None, None 154 | if item_type is gtk.ImageMenuItem and label: 155 | menu_item = gtk.ImageMenuItem.new_with_label(label) 156 | menu_item.set_always_show_image(True) 157 | if '/' in image: 158 | icon = gtk.Image.new_from_file(image) 159 | else: 160 | icon = gtk.Image.new_from_icon_name(image, 48) 161 | menu_item.set_image(icon) 162 | elif item_type is gtk.ImageMenuItem and not label: 163 | menu_item = gtk.ImageMenuItem() 164 | menu_item.set_always_show_image(True) 165 | if '/' in image: 166 | icon = gtk.Image.new_from_file(image) 167 | else: 168 | icon = gtk.Image.new_from_icon_name(image, 16) 169 | menu_item.set_image(icon) 170 | elif item_type is gtk.MenuItem: 171 | menu_item = gtk.MenuItem(label) 172 | elif item_type is gtk.SeparatorMenuItem: 173 | menu_item = gtk.SeparatorMenuItem() 174 | if action: 175 | menu_item.connect('activate', action, *args) 176 | 177 | # small addition for this specific indicator 178 | if label: 179 | if 'Partition' and 'Usage' in label: 180 | self.mounted_devs[label] = menu_item 181 | 182 | menu_obj.append(menu_item) 183 | menu_item.show() 184 | 185 | def get_fields(self, *args): 186 | """ Returns info field items necessary for 187 | each entry, padded depending on 188 | the system font used - default or monospace. 189 | Other font types are not covered 190 | """ 191 | font = self.gsettings_get( 192 | 'org.gnome.desktop.interface', 193 | None, 194 | 'font-name' 195 | ) 196 | font = str(font).replace("'", "") 197 | 198 | fields = [i + ':' for i in self.fields] 199 | if 'Ubuntu' in font and font.split()[1].isdigit(): 200 | # Thanks to Mateo for figuring out these numbers 201 | fields[0] = fields[0] + " " * 11 202 | fields[1] = fields[1] + " " * 20 203 | fields[2] = fields[2] + " " * 19 204 | fields[3] = fields[3] + " " * 4 205 | fields[4] = fields[4] + " " * 8 206 | fields[5] = fields[5] + " " * 18 207 | 208 | elif 'Mono' in font: 209 | for index, item in enumerate(fields): 210 | fields[index] = self.pad_string(item, 20, " ") 211 | return fields 212 | 213 | def make_label(self, info): 214 | fields = self.get_fields() 215 | lines = [None] * 6 216 | lines[0] = fields[0] + info[0] 217 | lines[2] = fields[2] + info[1] 218 | lines[3] = fields[3] + info[2] 219 | lines[5] = fields[5] + info[3] + '%' 220 | 221 | hr_value = self.get_human_readable(int(info[4])) 222 | lines[5] = lines[5] + ' (' + hr_value + ')' 223 | 224 | alias = self.find_alias(info[0]) 225 | if alias: 226 | lines[1] = fields[1] + alias 227 | else: 228 | lines[1] = None 229 | lines.append(self.make_usage_bar(info[3])) 230 | 231 | if info[5]: 232 | lines[4] = fields[4] + str(info[5]) 233 | else: 234 | lines[4] = fields[4] + "unknown" 235 | 236 | for j, f in enumerate(self.prefs['fields']): 237 | if not f: 238 | lines[j] = None 239 | 240 | return lines 241 | 242 | def make_menu(self, *args): 243 | """ generates entries in the indicator""" 244 | if hasattr(self, 'app_menu'): 245 | for item in self.app_menu.get_children(): 246 | self.app_menu.remove(item) 247 | self.app_menu = gtk.Menu() 248 | self.mounted_devs = {} 249 | 250 | drive_icon = 'gnome-dev-harddisk' 251 | usb_icon = 'media-removable' 252 | optical_icon = 'media-optical' 253 | 254 | icon_theme = self.gsettings_get( 255 | 'org.gnome.desktop.interface', None, 'icon-theme' 256 | ) 257 | if str(icon_theme) == "'ubuntukylin-icon-theme'": 258 | usb_icon = 'drive-harddisk-usb' 259 | 260 | # TODO: rewrite this into a function 261 | # MOUNTED 262 | # list of tuples,(partition,drive,mountpoint,pcent use,byte 263 | # use,fs_type) 264 | partitions = self.get_partitions() 265 | 266 | add_unmount_button = False 267 | mounted_devices_list = [i[0] for i in partitions if i[2] != '/'] 268 | 269 | if mounted_devices_list: 270 | add_unmount_button = True 271 | 272 | fields = self.get_fields() 273 | for i in partitions: 274 | label_lines = self.make_label(i) 275 | icon = drive_icon 276 | if self.get_bus_type(i[1]) == 'usb': 277 | icon = usb_icon 278 | if 'iso' in i[5]: 279 | icon = optical_icon 280 | 281 | label = '\n'.join([l for l in label_lines if l]) 282 | 283 | contents = [self.app_menu, gtk.ImageMenuItem, icon, 284 | label, self.open_mountpoint, [i[2]] 285 | ] 286 | self.add_menu_item(*contents) 287 | 288 | contents = [self.app_menu, gtk.SeparatorMenuItem, None, 289 | None, None, [None] 290 | ] 291 | self.add_menu_item(*contents) 292 | 293 | self.unmounted = gtk.MenuItem('Unmounted Partitions') 294 | self.unmounted_submenu = gtk.Menu() 295 | self.unmounted.set_submenu(self.unmounted_submenu) 296 | 297 | # UNMOUNTED 298 | for i in self.get_unmounted_partitions(): 299 | 300 | part = "Partition: " + i[0] 301 | alias = self.find_alias(i[0]) 302 | drive = "\nDrive: " + i[1] 303 | fs_type = str(self.get_filesystem_type(i[0])) 304 | 305 | icon = drive_icon 306 | if fs_type: 307 | filesystem = "\nFilesystem: " + fs_type 308 | # icon = 'drive-removable-media' 309 | # icon = 'drive-multidisk' 310 | # icon = 'drive-harddisk-system' 311 | if fs_type == 'swap': 312 | filesystem = filesystem + " NOT MOUNTABLE" 313 | icon = 'dialog-error' 314 | if 'iso' in fs_type: 315 | icon = optical_icon 316 | else: 317 | filesystem = "\nFilesystem: unknown" 318 | 319 | if self.get_bus_type(i[1]) == 'usb': 320 | icon = 'drive-removable-media-usb' 321 | 322 | label = part + drive + filesystem 323 | 324 | if alias: 325 | alias = "\nAlias: " + alias 326 | label = part + alias + drive + filesystem 327 | 328 | contents = [ 329 | self.unmounted_submenu, gtk.ImageMenuItem, icon, 330 | label, self.mount_partition, [i[0]] 331 | ] 332 | self.add_menu_item(*contents) 333 | contents = [ 334 | self.unmounted_submenu, gtk.SeparatorMenuItem, None, 335 | None, None, [None] 336 | ] 337 | self.add_menu_item(*contents) 338 | 339 | self.app_menu.append(self.unmounted) 340 | self.unmounted.show() 341 | 342 | self.separator = gtk.SeparatorMenuItem() 343 | self.app_menu.append(self.separator) 344 | self.separator.show() 345 | 346 | contents = [self.app_menu, gtk.ImageMenuItem, 'text-editor', 347 | 'Make Alias', self.make_alias, [None] 348 | ] 349 | self.add_menu_item(*contents) 350 | 351 | contents = [self.app_menu, gtk.ImageMenuItem, 'media-eject-symbolic', 352 | 'Unmount a mounted non-root partition', 353 | self.unmount_partition, mounted_devices_list 354 | ] 355 | self.add_menu_item(*contents) 356 | 357 | contents = [self.app_menu, gtk.ImageMenuItem, 'gnome-disks', 358 | 'Open Disks Utility', self.open_disks_utility, [None] 359 | ] 360 | self.add_menu_item(*contents) 361 | contents = [self.app_menu, gtk.ImageMenuItem, 'gtk-preferences', 362 | 'Preferences', self.preferences_dialog, [None] 363 | ] 364 | self.add_menu_item(*contents) 365 | contents = [self.app_menu, gtk.ImageMenuItem, 'exit', 366 | 'Quit', self.quit, [None] 367 | ] 368 | self.add_menu_item(*contents) 369 | contents = None 370 | self.app.set_menu(self.app_menu) 371 | 372 | def find_menu_child(self, menu, label): 373 | for child in menu.get_children(): 374 | if label in child.get_label(): 375 | return child 376 | 377 | def preferences_dialog(self, *args): 378 | 379 | def _toggle_field(*args): 380 | pref_vals = args[-1] 381 | pref_vals[args[-2]] = args[0].get_state() 382 | # self.make_menu() 383 | 384 | def _ok_action(*args): 385 | self.prefs['fields'] = copy.deepcopy(args[-2]) 386 | self.prefs['autostart'] = args[-1][0] 387 | self.process_autostart_file() 388 | self.write_prefs_file() 389 | glib.timeout_add_seconds(1, self.make_menu) 390 | window.hide() 391 | 392 | def _set_defaults(*args): 393 | self.prefs['fields'] = [True] * 5 394 | self.prefs['autostart'] = False 395 | os.unlink(self.prefs_file) 396 | window.hide() 397 | 398 | def _toggle_autostart(*args): 399 | obj = args[-1] 400 | obj[0] = args[0].get_state() 401 | 402 | def _cancel_action(*args): 403 | window.hide() 404 | 405 | prefs = copy.deepcopy(self.prefs['fields']) 406 | autostart = [copy.deepcopy(self.prefs['autostart'])] 407 | window = gtk.Window() 408 | window.set_border_width(40) 409 | vert = gtk.Orientation.VERTICAL 410 | horz = gtk.Orientation.HORIZONTAL 411 | 412 | main_box = gtk.Box(orientation=vert, spacing=20) 413 | listbox = gtk.ListBox() 414 | listbox.set_selection_mode(gtk.SelectionMode.NONE) 415 | main_box.pack_start(listbox, True, True, 0) 416 | window.add(main_box) 417 | 418 | row = gtk.ListBoxRow() 419 | listbox.add(row) 420 | label = gtk.Label('', xalign=0) 421 | label.set_markup("INFO FIELDS") 422 | row.add(label) 423 | 424 | for i, f in enumerate(self.fields): 425 | if i == 0 or i == 5: 426 | continue 427 | row = gtk.ListBoxRow() 428 | listbox.add(row) 429 | aux_box1 = gtk.Box(orientation=horz, spacing=20) 430 | aux_box2 = gtk.Box(orientation=vert, spacing=20) 431 | aux_box1.pack_start(aux_box2, True, True, 0) 432 | 433 | switch = gtk.Switch() 434 | label = gtk.Label(f, xalign=0) 435 | 436 | switch.props.valign = gtk.Align.CENTER 437 | if self.prefs['fields'][i]: 438 | switch.set_state(True) 439 | switch.connect('notify::active', _toggle_field, i, prefs) 440 | 441 | aux_box1.pack_start(switch, False, True, 0) 442 | aux_box2.pack_start(label, True, True, 0) 443 | row.add(aux_box1) 444 | 445 | row = gtk.ListBoxRow() 446 | listbox.add(row) 447 | label = gtk.Label('', xalign=0) 448 | label.set_markup("OTHER OPTIONS") 449 | row.add(label) 450 | 451 | row = gtk.ListBoxRow() 452 | listbox.add(row) 453 | aux_box = gtk.Box(orientation=horz, spacing=20) 454 | switch = gtk.Switch() 455 | if self.prefs['autostart']: 456 | switch.set_state(True) 457 | switch.connect('notify::active', _toggle_autostart, autostart) 458 | label = gtk.Label('Autostart', xalign=0) 459 | aux_box.pack_start(switch, False, True, 0) 460 | aux_box.pack_start(label, True, True, 0) 461 | row.add(aux_box) 462 | 463 | row = gtk.ListBoxRow() 464 | listbox.add(row) 465 | aux_box = gtk.Box(orientation=horz, spacing=20) 466 | for i in ['Set Defaults', 'Cancel', 'OK']: 467 | button = gtk.Button.new_with_label(i) 468 | if i == 'OK': 469 | button.connect('clicked', _ok_action, prefs, autostart) 470 | if i == 'Cancel': 471 | button.connect('clicked', _cancel_action, autostart) 472 | if i == 'Set Defaults': 473 | button.connect('clicked', _set_defaults) 474 | aux_box.pack_start(button, False, True, 0) 475 | row.add(aux_box) 476 | 477 | window.connect("delete-event", _cancel_action) 478 | listbox.show_all() 479 | window.show_all() 480 | 481 | def make_usage_bar(self, usage): 482 | """ creates usage bar out of unicode characters""" 483 | fill = float(usage) 484 | fill = int(fill / 10) 485 | # \u25A7 and \u25A1; maybe 2588 486 | # return u'\u2593'*fill + u'\u2591'*(10-fill) 487 | return u'\u2588' * fill + u'\u2592' * (10 - fill) 488 | 489 | def get_human_readable(self, size): 490 | """ converts size in bytes to 491 | human readable value in powers of 1024. 492 | Essentially, same as what df -h gives, or 493 | """ 494 | prefix = ['B', 'KiB', 'MiB', 'GiB', 495 | 'TiB', 'PiB', 'EiB', 496 | 'ZiB', 'YiB' 497 | ] 498 | counter = 0 499 | while size / 1024 > 0.9: 500 | counter = counter + 1 501 | size = size / 1024 502 | 503 | return str(round(size, 2)) + " " + prefix[counter] 504 | 505 | def pad_string(self, orig, length, pad): 506 | """ Pads characters to string to make it 507 | have specific length and returns it""" 508 | return orig + pad * (length - len(orig)) 509 | 510 | def send_notif(self, note_obj, title, text): 511 | note_obj.update(title, text) 512 | note_obj.show() 513 | 514 | def mount_partition(self, *args): 515 | try: 516 | cmd = ['udisksctl', 'mount', '-b', '/dev/' + args[-1]] 517 | subprocess.check_output(cmd, stderr=subprocess.STDOUT) 518 | except subprocess.CalledProcessError as e: 519 | self.send_notif(self.note, 'Mounting Error', e.output.decode()) 520 | print(str(e)) 521 | pass 522 | 523 | def unmount_partition(self, *args): 524 | print(args) 525 | vals = '|'.join(args[1:]) 526 | cmd = ['zenity', '--forms', '--title', 527 | 'Unmount a partition (excluding / )', '--text', '', 528 | '--add-combo', 'Partition', '--combo-values', vals 529 | ] 530 | choice = self.run_cmd(cmd) 531 | if not choice: 532 | return 533 | try: 534 | cmd = ['udisksctl', 'unmount', '-b', 535 | os.path.join('/dev/', choice.decode().strip())] 536 | subprocess.check_output(cmd, stderr=subprocess.STDOUT) 537 | except subprocess.CalledProcessError as e: 538 | self.send_notif(self.note, 'Mounting Error', e.output.decode()) 539 | print(str(e)) 540 | pass 541 | 542 | def get_filesystem_type(self, dev): 543 | args = ['system', 544 | 'org.freedesktop.UDisks2', 545 | '/org/freedesktop/UDisks2/block_devices/' + dev, 546 | 'org.freedesktop.UDisks2.Block', 547 | 'IdType' 548 | ] 549 | try: 550 | return str(self.get_dbus_property(*args)) 551 | except Exception as e: 552 | print(str(e)) 553 | return None 554 | 555 | def get_uuid(self,*args): 556 | """ Obtains UUID of a partition""" 557 | # Fixes https://github.com/SergKolo/udisks-indicator/issues/6 558 | dbus_call = ['system', 'org.freedesktop.UDisks2', 559 | '/org/freedesktop/UDisks2/block_devices/' + args[-1], 560 | 'org.freedesktop.UDisks2.Block', 'IdUUID' 561 | ] 562 | return self.get_dbus_property(*dbus_call) 563 | 564 | def get_bus_type(self, drive): 565 | """ Determines bus type of the bus via 566 | which a drive is connected """ 567 | return self.get_dbus_property( 568 | 'system', 'org.freedesktop.UDisks2', 569 | '/org/freedesktop/UDisks2/drives/' + drive, 570 | 'org.freedesktop.UDisks2.Drive', 'ConnectionBus' 571 | ) 572 | 573 | def get_mountpoint_usage(self, mountpoint): 574 | """ performs statvfs syscall and calculates usage 575 | from the amount of filesystem blocks used. Returns 576 | tuple of percentage and actual byte size used""" 577 | fs = statvfs(mountpoint) 578 | used_blocks = float(fs.f_blocks) - float(fs.f_bfree) 579 | used_bytes = int(fs.f_bsize * used_blocks) 580 | pcent_use = 100 * (used_blocks / float(fs.f_blocks)) 581 | return (str("{0:.2f}".format(pcent_use)), 582 | str(used_bytes) 583 | ) 584 | 585 | def get_network_entries(self): 586 | result = [] 587 | lines = [] 588 | proc = subprocess.Popen(["df", "--block-size=1"], stdout=subprocess.PIPE) 589 | 590 | for line in proc.stdout.readlines(): 591 | lines.append(line.strip().split()) 592 | 593 | for entry in lines: 594 | if "//" in str(entry[0]): 595 | result.append([x.decode('utf-8') for x in entry]) 596 | return result 597 | 598 | def get_network_drive_type(self, network_drive_name): 599 | lines = [] 600 | proc = subprocess.Popen('mount', stdout=subprocess.PIPE) 601 | 602 | for line in proc.stdout.readlines(): 603 | lines.append(line.strip().split()) 604 | 605 | for entry in lines: 606 | if network_drive_name in str(entry[0]): 607 | return ([x.decode('utf-8') for x in entry])[4] 608 | 609 | def append_network_shares(self, partitions): 610 | network_entries = self.get_network_entries() 611 | 612 | for network_entry in network_entries: 613 | new_element=[network_entry[0], "Network", network_entry[5], '?', '?', '-'] 614 | percentage_free = network_entry[4][0:2] 615 | total_size = network_entry[1] 616 | 617 | new_element[3] = str(percentage_free) 618 | new_element[4] = str(total_size) 619 | new_element[5] = self.get_network_drive_type(network_entry[0]) 620 | partitions.append(tuple(new_element)) 621 | return partitions 622 | 623 | def get_partitions(self): 624 | contents = [ 625 | 'system', 'org.freedesktop.UDisks2', '/org/freedesktop/UDisks2', 626 | 'org.freedesktop.DBus.ObjectManager', 'GetManagedObjects', None 627 | ] 628 | objects = self.get_dbus(*contents) 629 | 630 | partitions = [] 631 | for item in objects: 632 | try: 633 | if 'block_devices' in str(item): 634 | contents = [ 635 | 'system', 'org.freedesktop.UDisks2', item, 636 | 'org.freedesktop.UDisks2.Block', 'Drive' 637 | ] 638 | drive = self.get_dbus_property(*contents) 639 | if drive == '/': 640 | continue 641 | 642 | mountpoint = self.get_mountpoint(item) 643 | if not mountpoint: 644 | continue 645 | mountpoint = mountpoint.replace('\x00', '') 646 | 647 | drive = str(drive).split('/')[-1] 648 | pcent_use, byte_use = self.get_mountpoint_usage(mountpoint) 649 | 650 | part = str(item.split('/')[-1]) 651 | 652 | fs_type = self.get_filesystem_type(part) 653 | info = (part, drive, mountpoint, 654 | pcent_use, byte_use, fs_type) 655 | partitions.append(info) 656 | except: 657 | pass 658 | partitions = self.append_network_shares(partitions) 659 | partitions.sort() 660 | return partitions 661 | 662 | def get_mountpoint(self, dev_path): 663 | try: 664 | contents = [ 665 | 'system', 'org.freedesktop.UDisks2', dev_path, 666 | 'org.freedesktop.UDisks2.Filesystem', 'MountPoints' 667 | ] 668 | data = self.get_dbus_property(*contents)[0] 669 | 670 | except: 671 | return None 672 | else: 673 | if len(data) > 0: 674 | return ''.join([chr(byte) for byte in data]) 675 | 676 | def get_unmounted_partitions(self): 677 | contents = [ 678 | 'system', 'org.freedesktop.UDisks2', 679 | '/org/freedesktop/UDisks2', 'org.freedesktop.DBus.ObjectManager', 680 | 'GetManagedObjects', None 681 | ] 682 | objects = self.get_dbus(*contents) 683 | 684 | partitions = [] 685 | for item in objects: 686 | try: 687 | if 'block_devices' in str(item): 688 | dbus_args = ['system', 'org.freedesktop.UDisks2', 689 | item, 'org.freedesktop.UDisks2.Block', 690 | 'Drive' 691 | ] 692 | drive = self.get_dbus_property(*dbus_args) 693 | if drive == '/': 694 | continue 695 | 696 | mountpoint = self.get_mountpoint(item) 697 | if mountpoint: 698 | continue 699 | 700 | drive = str(drive).split('/')[-1] 701 | part = str(item.split('/')[-1]) 702 | if not part[-1].isdigit(): 703 | continue 704 | partitions.append((part, drive)) 705 | # print(partitions) 706 | 707 | except Exception as e: 708 | # print(e) 709 | pass 710 | 711 | partitions.sort() 712 | return partitions 713 | 714 | def get_dbus(self, bus_type, obj, path, interface, method, arg): 715 | """ utility: executes dbus method on specific interface""" 716 | if bus_type == "session": 717 | bus = dbus.SessionBus() 718 | if bus_type == "system": 719 | bus = dbus.SystemBus() 720 | proxy = bus.get_object(obj, path) 721 | method = proxy.get_dbus_method(method, interface) 722 | if arg: 723 | return method(arg) 724 | else: 725 | return method() 726 | 727 | def get_dbus_property(self, bus_type, obj, path, iface, prop): 728 | """ utility:reads properties defined on specific dbus interface""" 729 | if bus_type == "session": 730 | bus = dbus.SessionBus() 731 | if bus_type == "system": 732 | bus = dbus.SystemBus() 733 | try: 734 | proxy = bus.get_object(obj, path) 735 | aux = 'org.freedesktop.DBus.Properties' 736 | props_iface = dbus.Interface(proxy, aux) 737 | props = props_iface.Get(iface, prop) 738 | return props 739 | except Exception as e: 740 | pass 741 | return "" 742 | 743 | 744 | def make_alias(self, *args): 745 | """ writes to config file user-defined alias""" 746 | partitions = [i[0] for i in self.get_partitions()] 747 | 748 | combo_values = '|'.join(partitions) 749 | command = ['zenity', '--forms', '--title', 750 | 'Make Alias', '--text', '', 751 | '--add-combo', 'Partition', '--combo-values', 752 | combo_values, '--add-entry', 'Alias'] 753 | user_input = self.run_cmd(command) 754 | if not user_input: 755 | return 756 | alias = user_input.decode().strip().split('|') 757 | 758 | existing_values = None 759 | if os.path.isfile(self.config_file): 760 | with open(self.config_file) as conf_file: 761 | try: 762 | existing_values = json.load(conf_file) 763 | except ValueError: 764 | pass 765 | 766 | with open(self.config_file, 'w') as conf_file: 767 | uuid = self.get_uuid(alias[0]) 768 | if existing_values: 769 | existing_values[uuid] = alias[1] 770 | else: 771 | existing_values = {uuid: alias[1]} 772 | json.dump(existing_values, conf_file, indent=4, sort_keys=True) 773 | 774 | def find_alias(self, part): 775 | """ searches the config file for partition alias""" 776 | uuid = self.get_uuid(part) 777 | if os.path.isfile(self.config_file): 778 | with open(self.config_file) as conf_file: 779 | try: 780 | aliases = json.load(conf_file) 781 | except ValueError: 782 | pass 783 | else: 784 | if uuid in aliases: 785 | return aliases[uuid] 786 | else: 787 | return None 788 | 789 | def process_autostart_file(self, *args): 790 | desktop_file = '.config/autostart/udisks-indicator.desktop' 791 | full_path = os.path.join( 792 | self.user_home, 793 | desktop_file 794 | ) 795 | 796 | if self.prefs['autostart'] and not os.path.exists(full_path): 797 | original = '/usr/share/applications/udisks-indicator.desktop' 798 | if os.path.exists(original): 799 | shutil.copyfile(original, full_path) 800 | elif not self.prefs['autostart'] and os.path.exists(full_path): 801 | os.unlink(full_path) 802 | 803 | def open_mountpoint(self, *args): 804 | pid = subprocess.Popen(['xdg-open', args[-1]]).pid 805 | 806 | def open_disks_utility(self, *args): 807 | pid = subprocess.Popen(['gnome-disks']).pid 808 | 809 | def gsettings_get(self, schema, path, key): 810 | """utility: get value of gsettings schema""" 811 | if path is None: 812 | gsettings = Gio.Settings.new(schema) 813 | else: 814 | gsettings = Gio.Settings.new_with_path(schema, path) 815 | return gsettings.get_value(key) 816 | 817 | def run_cmd(self, cmdlist): 818 | """ utility: reusable function for running external commands """ 819 | new_env = dict(os.environ) 820 | new_env['LC_ALL'] = 'C' 821 | try: 822 | stdout = subprocess.check_output(cmdlist, env=new_env) 823 | except subprocess.CalledProcessError: 824 | pass 825 | else: 826 | if stdout: 827 | return stdout 828 | 829 | def run(self): 830 | """ Launches the indicator """ 831 | try: 832 | gtk.main() 833 | except KeyboardInterrupt: 834 | pass 835 | 836 | def quit(self, *args): 837 | """ closes indicator """ 838 | gtk.main_quit() 839 | 840 | 841 | def main(): 842 | """ defines program entry point """ 843 | indicator = UdisksIndicator() 844 | indicator.run() 845 | 846 | if __name__ == '__main__': 847 | main() 848 | -------------------------------------------------------------------------------- /udisks-indicator.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Version=1.0 3 | Name=Udisks Indicator 4 | Comment=Indicator for reporting partition information 5 | Exec=udisks-indicator 6 | Type=Application 7 | Icon=drive-harddisk-symbolic.svg 8 | Terminal=false 9 | --------------------------------------------------------------------------------