├── simon ├── __init__.py ├── stats.py └── simon.py ├── requirements.txt ├── screenshots └── dark.png ├── .gitignore ├── bin └── simon ├── LICENSE ├── setup.py └── README.md /simon/__init__.py: -------------------------------------------------------------------------------- 1 | from .simon import Simon 2 | 3 | __version__ = '0.1.1' 4 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | psutil==5.2.0 2 | pyobjc-core==3.2.1 3 | pyobjc-framework-Cocoa==3.2.1 4 | -------------------------------------------------------------------------------- /screenshots/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/half0wl/simon/HEAD/screenshots/dark.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | .venv 3 | *.pyc 4 | __pycache__/ 5 | *.egg-info/ 6 | dist/ 7 | build/ 8 | -------------------------------------------------------------------------------- /bin/simon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | from PyObjCTools import AppHelper 5 | 6 | from simon import Simon 7 | 8 | 9 | def suppress_dock_icon(): 10 | import objc, plistlib 11 | path_to_current_bundle = objc.currentBundle().bundlePath() 12 | path_to_plist = '{}/Contents/Info.plist'.format(path_to_current_bundle) 13 | plist = plistlib.readPlist(path_to_plist) 14 | plist['LSUIElement'] = '1' 15 | plistlib.writePlist(plist, path_to_plist) 16 | print('Done! Run Simon again.') 17 | 18 | 19 | if __name__ == '__main__': 20 | if (len(sys.argv) > 1) and (sys.argv[1] == '--suppress-dock-icon'): 21 | suppress_dock_icon() 22 | else: 23 | app = Simon.sharedApplication() 24 | AppHelper.runEventLoop() 25 | -------------------------------------------------------------------------------- /simon/stats.py: -------------------------------------------------------------------------------- 1 | import psutil 2 | import math 3 | 4 | 5 | def bytes2human(n): 6 | # Credits to /u/cyberspacecowboy on reddit 7 | # https://www.reddit.com/r/Python/comments/5xukpd/-/dem5k12/ 8 | symbols = (' B', ' KiB', ' MiB', ' GiB', ' TiB', ' PiB', ' EiB', ' ZiB', 9 | ' YiB') 10 | i = math.floor(math.log(abs(n)+1, 2) / 10) 11 | return '%.1f%s' % (n/2**(i*10), symbols[int(i)]) 12 | 13 | 14 | def cpu_usage(): 15 | return psutil.cpu_percent() 16 | 17 | 18 | def ram_usage(): 19 | return psutil.virtual_memory().percent 20 | 21 | 22 | def available_memory(): 23 | return bytes2human(psutil.virtual_memory().available) 24 | 25 | 26 | def disk_read(): 27 | return bytes2human(psutil.disk_io_counters().read_bytes) 28 | 29 | 30 | def disk_written(): 31 | return bytes2human(psutil.disk_io_counters().write_bytes) 32 | 33 | 34 | def network_recv(): 35 | return bytes2human(psutil.net_io_counters().bytes_recv) 36 | 37 | 38 | def network_sent(): 39 | return bytes2human(psutil.net_io_counters().bytes_sent) 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ray Chen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 18 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 19 | DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | from simon import __version__ 4 | 5 | 6 | setup( 7 | name='simon_mac', 8 | version=__version__, 9 | author='Ray Chen', 10 | author_email='ray@half0wl.com', 11 | packages=['simon'], 12 | scripts=['bin/simon'], 13 | url='http://pypi.python.org/pypi/simon_mac/', 14 | license='MIT', 15 | description='Simple menubar system monitor for macOS.', 16 | long_description='Visit https://github.com/half0wl/simon for info.', 17 | install_requires=[ 18 | 'psutil >= 5.2.0', 19 | 'pyobjc-core >= 3.2.1', 20 | 'pyobjc-framework-Cocoa >= 3.2.1', 21 | ], 22 | classifiers=[ 23 | 'Development Status :: 3 - Alpha', 24 | 'Environment :: MacOS X', 25 | 'Environment :: MacOS X :: Cocoa', 26 | 'Intended Audience :: Developers', 27 | 'Intended Audience :: End Users/Desktop', 28 | 'License :: OSI Approved :: MIT License', 29 | 'Operating System :: MacOS', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.6', 33 | 'Topic :: System :: Monitoring', 34 | 'Topic :: Utilities', 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simon 2 | 3 | Simple menubar system monitor for macOS, written in Python with pyobjc. 4 | 5 | ![Simon Screenshot](screenshots/dark.png) 6 | 7 | Only tested on macOS Sierra, should work for El Capitan. Supports Python 2.7 8 | and 3.6, versions in between hasn't been tested. 9 | 10 | ## Installation & Usage 11 | 12 | Install with pip: 13 | 14 | ```bash 15 | $ pip install simon_mac 16 | ``` 17 | 18 | To run Simon: 19 | 20 | ```bash 21 | $ simon 22 | Simon is now running. 23 | CTRL+C does not work here. 24 | You can quit through the menubar (Simon -> Quit). 25 | ``` 26 | 27 | To remove the Python rocketship icon from your dock (Note: not everyone will 28 | have the dock icon due to differences in Python installations. This **should** 29 | work, but if it doesn't, please open an issue.): 30 | 31 | ```bash 32 | $ simon --suppress-dock-icon 33 | Done! Run Simon again. 34 | ``` 35 | 36 | To run Simon in the background, use `nohup`: 37 | 38 | ```bash 39 | $ nohup simon & 40 | ``` 41 | 42 | To quit Simon, quit through the menubar (Simon -> Quit). 43 | 44 | ## Todo / Upcoming 45 | 46 | * More stats - battery, temperature, etc. 47 | * Measure impact on system resources 48 | * Preferences/settings: allow user to set update interval, etc. 49 | * ... 50 | 51 | ## License 52 | 53 | MIT 54 | -------------------------------------------------------------------------------- /simon/simon.py: -------------------------------------------------------------------------------- 1 | from Foundation import NSTimer, NSRunLoop 2 | from AppKit import NSApplication, NSStatusBar, NSMenu, NSMenuItem, \ 3 | NSEventTrackingRunLoopMode 4 | 5 | from .stats import cpu_usage, ram_usage, available_memory, disk_read, \ 6 | disk_written, network_recv, network_sent 7 | 8 | 9 | class Simon(NSApplication): 10 | 11 | def finishLaunching(self): 12 | self._setup_menuBar() 13 | 14 | # Create a timer which fires the update_ method every 1second, 15 | # and add it to the runloop 16 | NSRunLoop.currentRunLoop().addTimer_forMode_( 17 | NSTimer 18 | .scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( 19 | 1, self, 'update:', '', True 20 | ), 21 | NSEventTrackingRunLoopMode 22 | ) 23 | 24 | print('Simon is now running.') 25 | print('CTRL+C does not work here.') 26 | print('You can quit through the menubar (Simon -> Quit).') 27 | 28 | def update_(self, timer): 29 | 30 | # System 31 | self.CPU_USAGE.setTitle_('CPU Usage: {}%'.format(cpu_usage())) 32 | self.RAM_USAGE.setTitle_('RAM Usage: {}%'.format(ram_usage())) 33 | self.RAM_AVAILABLE.setTitle_('Available Memory: {}'.format( 34 | available_memory()) 35 | ) 36 | 37 | # Disk I/O 38 | self.DATA_READ.setTitle_('Read: {}'.format(disk_read())) 39 | self.DATA_WRITTEN.setTitle_('Written: {}'.format(disk_written())) 40 | 41 | # Network 42 | self.NETWORK_RECV.setTitle_('Received: {}'.format(network_recv())) 43 | self.NETWORK_SENT.setTitle_('Sent: {}'.format(network_sent())) 44 | 45 | def _setup_menuBar(self): 46 | statusBar = NSStatusBar.systemStatusBar() 47 | self.statusItem = statusBar.statusItemWithLength_(-1) 48 | self.menuBar = NSMenu.alloc().init() 49 | 50 | self.statusItem.setTitle_('Simon') 51 | 52 | # Labels/buttons 53 | self.SYSTEM = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_( 54 | 'System', 'doNothing:', '' 55 | ) 56 | self.DISKIO = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_( 57 | 'Disk I/O', 'doNothing:', '' 58 | ) 59 | self.NETWORK = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_( 60 | 'Network', 'doNothing:', '' 61 | ) 62 | self.QUIT = NSMenuItem.alloc().initWithTitle_action_keyEquivalent_( 63 | 'Quit', 'terminate:', '' 64 | ) 65 | 66 | # System 67 | self.CPU_USAGE = self._create_empty_menu_item() 68 | self.RAM_USAGE = self._create_empty_menu_item() 69 | self.RAM_AVAILABLE = self._create_empty_menu_item() 70 | 71 | # Disk I/O 72 | self.DATA_READ = self._create_empty_menu_item() 73 | self.DATA_WRITTEN = self._create_empty_menu_item() 74 | 75 | # Network 76 | self.NETWORK_RECV = self._create_empty_menu_item() 77 | self.NETWORK_SENT = self._create_empty_menu_item() 78 | 79 | ''' 80 | Add our items to the menuBar - yields the following output: 81 | 82 | Simon 83 | System 84 | CPU Usage 85 | RAM Usage 86 | Available Memory 87 | Disk I/O 88 | Read 89 | Written 90 | Network 91 | Received 92 | Sent 93 | ----------------------- 94 | Quit 95 | ''' 96 | self.menuBar.addItem_(self.SYSTEM) # system label 97 | self.menuBar.addItem_(self.CPU_USAGE) 98 | self.menuBar.addItem_(self.RAM_USAGE) 99 | self.menuBar.addItem_(self.RAM_AVAILABLE) 100 | 101 | self.menuBar.addItem_(self.DISKIO) # disk I/O label 102 | self.menuBar.addItem_(self.DATA_READ) 103 | self.menuBar.addItem_(self.DATA_WRITTEN) 104 | 105 | self.menuBar.addItem_(self.NETWORK) # network label 106 | self.menuBar.addItem_(self.NETWORK_RECV) 107 | self.menuBar.addItem_(self.NETWORK_SENT) 108 | 109 | self.menuBar.addItem_(NSMenuItem.separatorItem()) # seperator 110 | self.menuBar.addItem_(self.QUIT) # quit button 111 | 112 | # Add menu to status bar 113 | self.statusItem.setMenu_(self.menuBar) 114 | 115 | def _create_empty_menu_item(self): 116 | return NSMenuItem \ 117 | .alloc().initWithTitle_action_keyEquivalent_('', '', '') 118 | 119 | def doNothing_(self, sender): 120 | # hack to enable menuItems by passing them this method as action 121 | # setEnabled_ isn't working, so this should do for now (achieves 122 | # the same thing) 123 | pass 124 | --------------------------------------------------------------------------------