class Unicron(object): def __init__(self): self.locations = [ 'User Agents', 'Global Agents', 'Global Daemons', 'System Agents', 'System Daemons' ] self.listItems = [] self.selected = {} # Preferences self.homedir = os.path.expanduser('~') self.prefsFolder = self.homedir + "/Library/Preferences/" self.prefsFile = "de.nelsonfritsch.unicron.plist" if os.path.isfile(self.prefsFolder + self.prefsFile): self.prefs = self._loadPrefs(self) else: self.prefs = dict(showSystemWarning=True, windowStyle='System') self._savePrefs(self) # Preferences Window self.prefsWindow = Window((300, 105), 'Preferences') self.styles = ['System', 'Light', 'Dark'] self.prefsWindow.styleTxt = TextBox((10, 10, -10, 20), "Window Style:") self.prefsWindow.style = PopUpButton((30, 35, -10, 20), self.styles, callback=self.prefsSetStyle) self.prefsWindow.restore = Button((10, 75, -10, 20), 'Restore Warnings', callback=self.prefsRestoreWarnings) # Main Window minsize = 285 self.w = Window((minsize, 400), 'Unicron', closable=True, fullSizeContentView=True, titleVisible=False, minSize=(minsize, minsize), maxSize=(600, 1200), autosaveName="UnicronMainWindow") self.pathList = NSPopUpButton.alloc().initWithFrame_( ((0, 0), (160, 20))) self.pathList.addItemsWithTitles_(self.locations) refreshIcon = NSImage.alloc().initWithSize_((32, 32)) sourceImage = NSImage.imageNamed_(NSImageNameRefreshTemplate) w, h = sourceImage.size() if w > h: diffx = 0 diffy = w - h else: diffx = h - w diffy = 0 maxSize = max([w, h]) refreshIcon.lockFocus() sourceImage.drawInRect_fromRect_operation_fraction_( NSMakeRect(diffx, diffy + 4, 22, 22), NSMakeRect(0, 0, maxSize, maxSize), NSCompositeSourceOver, 1) refreshIcon.unlockFocus() refreshIcon.setTemplate_(True) toolbarItems = [ dict(itemIdentifier="Daemons", label="Daemons", toolTip="Daemon Group", view=self.pathList, callback=self.populateList), dict(itemIdentifier="image", label="Image", imageObject=refreshIcon, callback=self.populateList), ] self.w.addToolbar("Unicron Toolbar", toolbarItems=toolbarItems, displayMode="icon") self.w.blend = Group((0, 0, 0, 0), blendingMode='behindWindow') self.listColumnDescriptions = [{ 'title': '', 'key': 'image', 'width': 25, 'typingSensitive': True, 'allowsSorting': True, 'cell': ImageListCell() }, { 'title': 'Name', 'key': 'name', 'typingSensitive': True, 'allowsSorting': True, }] self.rowHeight = 20 self.w.list = List((0, 37, -0, 0), items=self.listItems, columnDescriptions=self.listColumnDescriptions, showColumnTitles=True, allowsEmptySelection=True, allowsMultipleSelection=False, autohidesScrollers=True, drawFocusRing=False, rowHeight=self.rowHeight, selectionCallback=self._selectionCallback, menuCallback=self._menuCallback) self.w.list._nsObject.setBorderType_(NSNoBorder) self.w.statusbar = Group((0, -26, 0, 0), blendingMode='behindWindow') self.w.statusbar.border = HorizontalLine((0, 0, 0, 1)) self.w.counter = TextBox((16, -20, -16, 15), '', alignment='center', sizeStyle='small') self.populateList(self) self.w.rowIndicator = Group((0, 0, 0, 10)) self.prefsSetStyle(self) self.w.open() def prefsSetStyle(self, sender): style = self.prefsWindow.style.getItem() self._changePref(self, 'windowStyle', style) if style == 'System': style = NSUserDefaults.standardUserDefaults().stringForKey_( 'AppleInterfaceStyle') if style == 'Dark': winAppearance = 'NSAppearanceNameVibrantDark' else: winAppearance = 'NSAppearanceNameVibrantLight' appearance = NSAppearance.appearanceNamed_(winAppearance) self.w._window.setAppearance_(appearance) self.prefsWindow._window.setAppearance_(appearance) def prefsRestoreWarnings(self, sender): self._changePref(self, 'showSystemWarning', True) def populateList(self, sender): self.selected.clear() self.w.list._removeSelection() item = self.pathList.titleOfSelectedItem() for i in range(len(self.w.list)): del self.w.list[0] thisItem = {} image = None id = os.getuid() systemWarning = "You should not edit or remove existing system's daemons. These jobs are required for a working macOS system." if item != 'Active Daemons': if item == 'User Agents': homedir = os.path.expanduser('~') path = homedir + '/Library/LaunchAgents' # If the folder doesn't exist in the user folder, create it try: os.listdir(path) except: os.mkdir(path) elif item == 'Global Agents': path = '/Library/LaunchAgents' elif item == 'Global Daemons': path = '/Library/LaunchDaemons' elif item == 'System Agents': path = '/System/Library/LaunchAgents' self._warning(self, systemWarning, "showSystemWarning") elif item == 'System Daemons': path = '/System/Library/LaunchDaemons' self._warning(self, systemWarning, "showSystemWarning") items = [] files = os.listdir(path) count = 0 for file in files: if file.endswith('.plist'): file = file.replace('.plist', '') try: pid = launchd.LaunchdJob(file).pid except: pid = False if launchd.LaunchdJob(file).exists() and pid != None: image = NSImage.imageNamed_(NSImageNameStatusAvailable) elif launchd.LaunchdJob(file).exists() and pid == None: image = NSImage.imageNamed_( NSImageNameStatusPartiallyAvailable) else: image = NSImage.imageNamed_(NSImageNameStatusNone) state = True thisItem['image'] = image thisItem['name'] = file self.w.list.append(thisItem) count += 1 self.w.counter.set(str(count) + ' Jobs') def _showInFinder(self, sender): file = self.selected['file'] subprocess.call(['open', '-R', '%s' % file], cwd='/', shell=False, universal_newlines=False) def _loadUnloadDaemon(self, sender, command): self.w.list.scrollToSelection() name = self.selected['name'] path = self.selected['file'] if bool(launchd.LaunchdJob(name).exists()): try: subprocess.call( ['launchctl', 'unload', '%s' % path], cwd='/', shell=False, universal_newlines=False) except: return else: try: subprocess.call( ['launchctl', 'load', '%s' % path], cwd='/', shell=False, universal_newlines=False) except: return self.populateList(self) def _removeDaemon(self, sender): self._loadUnloadDaemon(self, 'unload') self._loadUnloadDaemon(self, 'remove') # def addGroup(self, sender, key, value): def _selectionCallback(self, sender): try: if not self.w.list.getSelection(): # Application did not finish loading yet pass else: # Get job name self.selected.clear() job = sender.get()[self.w.list.getSelection()[0]] self.selected['name'] = job['name'] self.valueGroups = [] # Get job path and file location item = self.pathList.titleOfSelectedItem() if 'User' in item: import getpass username = getpass.getuser() user = username path = '/Users/%s/Library/Launch' % username elif 'Global' in item: user = '******' path = '/Library/Launch' elif 'System' in item: user = '******' path = '/System/Library/Launch' if 'Agents' in item: path += 'Agents/' else: path += 'Daemons/' self.selected['path'] = path self.selected['file'] = str(self.selected['path'].replace( ' ', '\ ')) + job['name'].replace(' ', '\ ') + '.plist' f = open(self.selected['file'], "r") self.selected['raw'] = str(f.read()) self.selected['short'] = ( self.selected['name'][:32] + '…') if len( self.selected['name']) > 32 else self.selected['name'] # Get status if job['image'] == NSImage.imageNamed_(NSImageNameStatusNone): status = None else: status = 'Available' self.selected['status'] = status index = sender.getSelection()[0] relativeRect = sender.getNSTableView().rectOfRow_(index) self.pop = Popover((300, 100)) self.pop.tabs = Tabs((20, 40, -20, -20), ["Editor", "Raw View"]) self.pop.tabs._nsObject.setTabViewType_(NSNoTabsNoBorder) self.pop.tabBtn = SegmentedButton( (10, 10, -10, 20), [dict(title="Editor"), dict(title="Raw View")], callback=self._segmentPressed, selectionStyle='one') self.pop.tabBtn.set(0) self.edit = self.pop.tabs[0] self.rawEdit = self.pop.tabs[1] self.rawEdit.editor = TextEditor((0, 0, -0, -45), text=self.selected['raw']) self.selected['dict'] = launchd.plist.read( self.selected['name']) # TODO: Add stackview to scrollview as group # Waiting for merge into master: https://github.com/robotools/vanilla/issues/132 self.edit.stack = VerticalStackGroup((0, 0, -0, -45)) for idx, (key, value) in enumerate( sorted(self.selected['dict'].items())): group = ValueGroup((0, 0, -0, -0), sender=self, key=key, value=value, idx=idx) self.valueGroups.append(group) self.edit.stack.addView( self.valueGroups[idx], 300, self.valueGroups[idx].getPosSize()[3]) self.pop.save = Button((20, -50, -20, 40), "Save", callback=self._savePlist) self.pop.save.enable(False) self.pop.open(parentView=sender.getNSTableView(), preferredEdge='right', relativeRect=relativeRect) except: pass # TODO def _savePlist(self, sender): return def _segmentPressed(self, sender): self.pop.tabs.set(self.pop.tabBtn.get()) def _menuCallback(self, sender): items = [] items.append(dict(title=self.selected['short'], enabled=False)) items.append("----") if self.selected['status'] == None: load, able = 'Load', 'Enable' else: load, able = 'Unload', 'Disable' loadCallback = partial(self._loadUnloadDaemon, command=load) ableCallback = partial(self._loadUnloadDaemon, command=able) items.append(dict(title=load, callback=loadCallback)) items.append(dict(title=able, callback=ableCallback)) items.append(dict(title="Show in Finder", callback=self._showInFinder)) items.append(dict(title="Refresh list", callback=self.populateList)) return items def _loadPrefs(self, sender): with open(self.prefsFolder + self.prefsFile, 'rb') as fp: self.prefs = plistlib.load(fp) return self.prefs def _savePrefs(self, sender): with open(self.prefsFolder + self.prefsFile, 'wb') as fp: plistlib.dump(self.prefs, fp) def _changePref(self, sender, key, value): self.prefs[key] = value self._savePrefs(self) def _warning(self, sender, warning, prefKey): if self.prefs.get(prefKey): self.warning = Sheet((400, 140), self.w) self.warning.img = ImageView((10, 10, 60, 60)) self.warning.img.setImage(imageNamed=NSImageNameCaution) self.warning.txt = TextBox((70, 10, -10, -40), "Warning\n" + warning) callback = partial(self._changePref, key=prefKey, value=not self.prefs.get(prefKey)) self.warning.check = CheckBox((70, 80, -10, 20), "Always show this warning", value=self.prefs.get(prefKey), callback=callback) self.warning.closeButton = Button((10, 110, -10, 20), "I understand", callback=self._closeWarning) self.warning.setDefaultButton(self.warning.closeButton) self.warning.center() self.w.list.enable(False) self.warning.open() def _closeWarning(self, sender): self.warning.close() self.w.list.enable(True) del self.warning
class PresetsEditor: def __init__(self, mainWindow, presetsList): self.presets = presetsList self.presetNames = [preset.name for preset in self.presets] self.selectedPreset = None self.selectedGroupIndex = None gutter = 10 left = 10 row = 10 colWidth = 275 listHeight = 115 col2Left = left + colWidth + gutter btnWidth = 85 btnHeight = 22 boxWidth = btnWidth * 2 + 15 windowWidth = col2Left + boxWidth + 10 self.w = Sheet((windowWidth, 550), mainWindow) self.w.presetsText = TextBox((left, row, colWidth, 20), "Presets:", sizeStyle="small") row += 17 self.w.presetsList = List((left, row, colWidth, listHeight), items=self.presetNames, allowsSorting=False, allowsMultipleSelection=False, allowsEmptySelection=False, selectionCallback=self.updatePresetInfo) self.w.presetCtrls = Box((col2Left, row, boxWidth, listHeight)) boxLeft = 0 boxRow = 0 self.w.presetCtrls.edit = TextBox((boxLeft, boxRow, btnWidth, 20), "Edit:", sizeStyle="small") boxRow += 15 self.w.presetCtrls.newBtn = Button((boxLeft, boxRow, btnWidth, btnHeight), "New", sizeStyle="small", callback=self.testerCB) boxRow += 22 self.w.presetCtrls.dupeBtn = Button((boxLeft, boxRow, btnWidth, btnHeight), "Duplicate", sizeStyle="small", callback=self.testerCB) boxRow += 22 self.w.presetCtrls.renameBtn = Button((boxLeft, boxRow, btnWidth, btnHeight), "Rename", sizeStyle="small", callback=self.testerCB) boxRow += 22 self.w.presetCtrls.delBtn = Button((boxLeft, boxRow, btnWidth, btnHeight), "Delete", sizeStyle="small", callback=self.testerCB) boxRow = 0 boxLeft += btnWidth + 7 self.w.presetCtrls.importText = TextBox((boxLeft, boxRow, btnWidth, 20), "Import:", sizeStyle="small") boxRow += 15 self.w.presetCtrls.importJSON = Button((boxLeft, boxRow, btnWidth, btnHeight), "JSON", sizeStyle="small", callback=self.testerCB) boxRow += 22 self.w.presetCtrls.importGroups = Button((boxLeft, boxRow, btnWidth, btnHeight), "Proof groups", sizeStyle="small", callback=self.testerCB) row += listHeight + 12 self.w.proofGroupsText = TextBox((left, row, colWidth, 20), "Proof groups:", sizeStyle="small") row += 17 listHeight = 150 self.w.proofGroupNames = List((left, row, colWidth, listHeight), items=[], allowsSorting=False, allowsMultipleSelection=False, selectionCallback=self.updateGroupContents) self.w.groupCtrls = Box((col2Left, row, boxWidth, listHeight)) boxLeft = 0 boxRow = 0 self.w.groupCtrls.newBtn = Button((boxLeft, boxRow, btnWidth, btnHeight), "New", sizeStyle="small", callback=self.testerCB) boxRow += 22 self.w.groupCtrls.dupeBtn = Button((boxLeft, boxRow, btnWidth, btnHeight), "Duplicate", sizeStyle="small", callback=self.testerCB) boxRow += 22 self.w.groupCtrls.rename = Button((boxLeft, boxRow, btnWidth, btnHeight), "Rename", sizeStyle="small", callback=self.testerCB) boxRow += 22 self.w.groupCtrls.delBtn = Button((boxLeft, boxRow, btnWidth, btnHeight), "Delete", sizeStyle="small", callback=self.testerCB) boxRow = 22 boxLeft += btnWidth + 7 + (btnWidth / 2 - 15) self.w.groupCtrls.upBtn = Button((boxLeft, boxRow, 30, btnHeight), "↑", sizeStyle="small", callback=self.testerCB) boxRow += 22 self.w.groupCtrls.dnBtn = Button((boxLeft, boxRow, 30, btnHeight), "↓", sizeStyle="small", callback=self.testerCB) row += listHeight + 12 self.w.groupContentsText = TextBox((left, row, colWidth, 20), "Group contents:", sizeStyle="small") row += 17 self.w.groupContents = TextEditor((left, row, -10, -36), text="", readOnly=True, callback=self.editGroupContents) self.w.groupContents.getNSTextView().setFont_(monoFont) # self.w.renameText = TextBox((left, row, colWidth, 20), # "Rename preset:", # sizeStyle="small") # self.w.renameEdit = EditText((left, row, colWidth, btnHeight)) row += 17 self.w.okButton = Button((windowWidth/2 - btnWidth/2, -31, btnWidth, btnHeight), "OK", callback=self.closeCB) self.w.setDefaultButton(self.w.okButton) self.updatePresetInfo() def closeCB(self, sender): self.w.close() def testerCB(self, sender): print("hit: %s" % sender) def updatePresetInfo(self, sender=None): """ Update self.w.proofGroupNames and self.w.renameEdit, and reset proofGroupNames selection and groupContents when user selects preset from list """ if sender is None: selectionIndex = 0 else: if not sender.getSelection(): return selectionIndex = sender.getSelection()[0] self.selectedPreset = self.presets[selectionIndex] self.w.proofGroupNames.set(self.selectedPreset.groupNames) self.w.proofGroupNames.setSelection([]) self._resetGroupContents() def updateGroupContents(self, sender): """ Update groupContents when user selects a proof group """ if not sender.getSelection(): self._resetGroupContents() return self.selectedGroupIndex = sender.getSelection()[0] selectedGroup = self.selectedPreset.groups[self.selectedGroupIndex] self.w.groupContents.set("\n".join(selectedGroup.contents)) self.w.groupContents.getNSTextView().setEditable_(True) def editGroupContents(self, sender): """ Set group contents to new contents """ newContents = sender.get().split("\n") self.selectedPreset.groups[self.selectedGroupIndex].contents = newContents def _resetGroupContents(self): """ Set w.groupContents to empty string and make it readOnly """ self.w.groupContents.set("") self.w.groupContents.getNSTextView().setEditable_(False)