diff --git a/ansible/library/osx_defaults.py b/ansible/library/osx_defaults.py new file mode 100644 index 0000000..0dd7ca8 --- /dev/null +++ b/ansible/library/osx_defaults.py @@ -0,0 +1,349 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, GeekChimp - Franck Nijhof +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +DOCUMENTATION = ''' +--- +module: osx_defaults +author: Franck Nijhof +short_description: osx_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible +description: + - osx_defaults allows users to read, write, and delete Mac OS X user defaults from Ansible scripts. + Mac OS X applications and other programs use the defaults system to record user preferences and other + information that must be maintained when the applications aren't running (such as default font for new + documents, or the position of an Info panel). +version_added: 1.8 +options: + domain: + description: + - The domain is a domain name of the form com.companyname.appname. + required: false + default: NSGlobalDomain + key: + description: + - The key of the user preference + required: true + type: + description: + - The type of value to write. + required: false + default: string + choices: [ "array", "bool", "boolean", "date", "float", "int", "integer", "string" ] + array_add: + description: + - Add new elements to the array for a key which has an array as its value. + required: false + default: string + choices: [ "true", "false" ] + value: + description: + - The value to write. Only required when state = present. + required: false + default: null + state: + description: + - The state of the user defaults + required: false + default: present + choices: [ "present", "absent" ] +notes: + - Apple Mac caches defaults. You may need to logout and login to apply the changes. +''' + +EXAMPLES = ''' +- osx_defaults: domain=com.apple.Safari key=IncludeInternalDebugMenu type=bool value=true state=present +- osx_defaults: domain=NSGlobalDomain key=AppleMeasurementUnits type=string value=Centimeters state=present +- osx_defaults: key=AppleMeasurementUnits type=string value=Centimeters +- osx_defaults: + key: AppleLanguages + type: array + value: ["en", "nl"] +- osx_defaults: domain=com.geekchimp.macable key=ExampleKeyToRemove state=absent +''' + +from datetime import datetime + +# exceptions --------------------------------------------------------------- {{{ +class OSXDefaultsException(Exception): + pass +# /exceptions -------------------------------------------------------------- }}} + +# class MacDefaults -------------------------------------------------------- {{{ +class OSXDefaults(object): + + """ Class to manage Mac OS user defaults """ + + # init ---------------------------------------------------------------- {{{ + """ Initialize this module. Finds 'defaults' executable and preps the parameters """ + def __init__(self, **kwargs): + + # Initial var for storing current defaults value + self.current_value = None + + # Just set all given parameters + for key, val in kwargs.iteritems(): + setattr(self, key, val) + + # Try to find the defaults executable + self.executable = self.module.get_bin_path( + 'defaults', + required=False, + opt_dirs=self.path.split(':'), + ) + + if not self.executable: + raise OSXDefaultsException("Unable to locate defaults executable.") + + # When state is present, we require a parameter + if self.state == "present" and self.value is None: + raise OSXDefaultsException("Missing value parameter") + + # Ensure the value is the correct type + self.value = self._convert_type(self.type, self.value) + + # /init --------------------------------------------------------------- }}} + + # tools --------------------------------------------------------------- {{{ + """ Converts value to given type """ + def _convert_type(self, type, value): + + if type == "string": + return str(value) + elif type in ["bool", "boolean"]: + if value.lower() in [True, 1, "true", "1", "yes"]: + return True + elif value.lower() in [False, 0, "false", "0", "no"]: + return False + raise OSXDefaultsException("Invalid boolean value: {0}".format(repr(value))) + elif type == "date": + try: + return datetime.strptime(value.split("+")[0].strip(), "%Y-%m-%d %H:%M:%S") + except ValueError: + raise OSXDefaultsException( + "Invalid date value: {0}. Required format yyy-mm-dd hh:mm:ss.".format(repr(value)) + ) + elif type in ["int", "integer"]: + if not str(value).isdigit(): + raise OSXDefaultsException("Invalid integer value: {0}".format(repr(value))) + return int(value) + elif type == "float": + try: + value = float(value) + except ValueError: + raise OSXDefaultsException("Invalid float value: {0}".format(repr(value))) + return value + elif type == "array": + if not isinstance(value, list): + raise OSXDefaultsException("Invalid value. Expected value to be an array") + return value + + raise OSXDefaultsException('Type is not supported: {0}'.format(type)) + + """ Converts array output from defaults to an list """ + @staticmethod + def _convert_defaults_str_to_list(value): + + # Split output of defaults. Every line contains a value + value = value.splitlines() + + # Remove first and last item, those are not actual values + value.pop(0) + value.pop(-1) + + # Remove extra spaces and comma (,) at the end of values + value = [re.sub(',$', '', x.strip(' ')) for x in value] + + return value + # /tools -------------------------------------------------------------- }}} + + # commands ------------------------------------------------------------ {{{ + """ Reads value of this domain & key from defaults """ + def read(self): + # First try to find out the type + rc, out, err = self.module.run_command([self.executable, "read-type", self.domain, self.key]) + + # If RC is 1, the key does not exists + if rc == 1: + return None + + # If the RC is not 0, then terrible happened! Ooooh nooo! + if rc != 0: + raise OSXDefaultsException("An error occurred while reading key type from defaults: " + out) + + # Ok, lets parse the type from output + type = out.strip().replace('Type is ', '') + + # Now get the current value + rc, out, err = self.module.run_command([self.executable, "read", self.domain, self.key]) + + # Strip output + out = out.strip() + + # An non zero RC at this point is kinda strange... + if rc != 0: + raise OSXDefaultsException("An error occurred while reading key value from defaults: " + out) + + # Convert string to list when type is array + if type == "array": + out = self._convert_defaults_str_to_list(out) + + # Store the current_value + self.current_value = self._convert_type(type, out) + + """ Writes value to this domain & key to defaults """ + def write(self): + + # We need to convert some values so the defaults commandline understands it + if type(self.value) is bool: + value = "TRUE" if self.value else "FALSE" + elif type(self.value) is int or type(self.value) is float: + value = str(self.value) + elif self.array_add and self.current_value is not None: + value = list(set(self.value) - set(self.current_value)) + elif isinstance(self.value, datetime): + value = self.value.strftime('%Y-%m-%d %H:%M:%S') + else: + value = self.value + + # When the type is array and array_add is enabled, morph the type :) + if self.type == "array" and self.array_add: + self.type = "array-add" + + # All values should be a list, for easy passing it to the command + if not isinstance(value, list): + value = [value] + + rc, out, err = self.module.run_command([self.executable, 'write', self.domain, self.key, '-' + self.type] + value) + + if rc != 0: + raise OSXDefaultsException('An error occurred while writing value to defaults: ' + out) + + """ Deletes defaults key from domain """ + def delete(self): + rc, out, err = self.module.run_command([self.executable, 'delete', self.domain, self.key]) + if rc != 0: + raise OSXDefaultsException("An error occurred while deleting key from defaults: " + out) + + # /commands ----------------------------------------------------------- }}} + + # run ----------------------------------------------------------------- {{{ + """ Does the magic! :) """ + def run(self): + + # Get the current value from defaults + self.read() + + # Handle absent state + if self.state == "absent": + print "Absent state detected!" + if self.current_value is None: + return False + self.delete() + return True + + # There is a type mismatch! Given type does not match the type in defaults + if self.current_value is not None and type(self.current_value) is not type(self.value): + raise OSXDefaultsException("Type mismatch. Type in defaults: " + type(self.current_value).__name__) + + # Current value matches the given value. Nothing need to be done. Arrays need extra care + if self.type == "array" and self.current_value is not None and not self.array_add and \ + set(self.current_value) == set(self.value): + return False + elif self.type == "array" and self.current_value is not None and self.array_add and \ + len(list(set(self.value) - set(self.current_value))) == 0: + return False + elif self.current_value == self.value: + return False + + # Change/Create/Set given key/value for domain in defaults + self.write() + return True + + # /run ---------------------------------------------------------------- }}} + +# /class MacDefaults ------------------------------------------------------ }}} + + +# main -------------------------------------------------------------------- {{{ +def main(): + module = AnsibleModule( + argument_spec=dict( + domain=dict( + default="NSGlobalDomain", + required=False, + ), + key=dict( + default=None, + ), + type=dict( + default="string", + required=False, + choices=[ + "array", + "bool", + "boolean", + "date", + "float", + "int", + "integer", + "string", + ], + ), + array_add=dict( + default=False, + required=False, + choices=BOOLEANS, + ), + value=dict( + default=None, + required=False, + ), + state=dict( + default="present", + required=False, + choices=[ + "absent", "present" + ], + ), + path=dict( + default="/usr/bin:/usr/local/bin", + required=False, + ) + ), + supports_check_mode=True, + ) + + domain = module.params['domain'] + key = module.params['key'] + type = module.params['type'] + array_add = module.params['array_add'] + value = module.params['value'] + state = module.params['state'] + path = module.params['path'] + + try: + defaults = OSXDefaults(module=module, domain=domain, key=key, type=type, + array_add=array_add, value=value, state=state, path=path) + changed = defaults.run() + module.exit_json(changed=changed) + except OSXDefaultsException as e: + module.fail_json(msg=e.message) + +# /main ------------------------------------------------------------------- }}} + +from ansible.module_utils.basic import * +main() diff --git a/ansible/roles/osx/tasks/defaults.yml b/ansible/roles/osx/tasks/defaults.yml index 5e01175..dfc3f9a 100644 --- a/ansible/roles/osx/tasks/defaults.yml +++ b/ansible/roles/osx/tasks/defaults.yml @@ -1,10 +1,16 @@ --- - include_vars: defaults.yml -- command: defaults write {{ item }} +# - command: defaults write {{ item }} +- osx_defaults: + domain: "{{ item.domain | default(omit) }}" + key: "{{ item.key }}" + type: "{{ item.type }}" + value: "{{ item.value }}" with_items: osx_defaults -- command: defaults -currentHost write NSGlobalDomain com.apple.mouse.tapBehavior -bool true # tap to click +# - command: defaults -currentHost write NSGlobalDomain com.apple.mouse.tapBehavior -bool true # tap to click + # - command: defaults write {{ item }} dontAutoLoad -array # /System/Library/CoreServices/Menu Extras/TimeMachine.menu # /System/Library/CoreServices/Menu Extras/Bluetooth.menu @@ -16,8 +22,8 @@ # - Set :FK_StandardViewSettings:IconViewSettings:arrangeBy grid # - Set :StandardViewSettings:IconViewSettings:arrangeBy grid -- command: killall {{ item }} - with_items: - - Finder - - Dock - - SystemUIServer +# - command: killall {{ item }} +# with_items: +# - Finder +# - Dock +# - SystemUIServer diff --git a/ansible/roles/osx/tasks/main.yml b/ansible/roles/osx/tasks/main.yml index 740a6ce..dc9fa6f 100644 --- a/ansible/roles/osx/tasks/main.yml +++ b/ansible/roles/osx/tasks/main.yml @@ -5,6 +5,7 @@ - include: casks.yml - include: fonts.yml - include: defaults.yml + tags: defaults - file: path=~/Library/KeyBindings state=directory - file: diff --git a/ansible/roles/osx/vars/defaults.yml b/ansible/roles/osx/vars/defaults.yml index 185e35e..c72b112 100644 --- a/ansible/roles/osx/vars/defaults.yml +++ b/ansible/roles/osx/vars/defaults.yml @@ -3,36 +3,146 @@ osx_defaults: # This is named `osx_defaults` since there's an Ansible conflict when this key # is named just `defaults`. - - -g NSDisableAutomaticTermination -bool true # Don't quit idle applications - - - NSGlobalDomain AppleFontSmoothing -int 2 # subpixel rendering on non-Apple LCDs - - NSGlobalDomain AppleKeyboardUIMode -int 3 # full keyboard access - - NSGlobalDomain AppleShowAllExtensions -bool true # show all extensions by default - - NSGlobalDomain KeyRepeat -int 2 # keyboard repeat rate - - NSGlobalDomain IinitialKeyRepeat -int 15 # delay before keyboard repeat - - NSGlobalDomain NSTableViewDefaultSizeMode -int 1 # set sidebar item size to small - - NSGlobalDomain NSQuitAlwaysKeepsWindows -bool false # disable resume - - NSGlobalDomain WebKitDeveloperExtras -bool true # add debug menu in web views - - NSGlobalDomain com.apple.mouse.tapBehavior -bool true # tap to click - - - com.apple.Safari IncludeInternalDebugMenu -bool true # enable Debug menu in Safari - - com.apple.dashboard mcx-disabled -bool true # disable dashboard - - com.apple.desktopservices DSDontWriteNetworkStores true # don't write .DS_Store to network volumes - - com.apple.dock autohide -bool true # automatically hide and show the dock - - com.apple.dock mineffect -string scale # minimize windows using the scale effect - - com.apple.dock mru-spaces -bool false # don't rearrange spaces - - com.apple.dock orientation -string left - - com.apple.dock wvous-bl-corner -int 10 # set the bottom left hot corner to sleep the display - - com.apple.driver.AppleBluetoothMultitouch.trackpad Clicking -bool true - - com.apple.driver.AppleBluetoothMultitouch.trackpad DragLock -int 1 - - com.apple.driver.AppleBluetoothMultitouch.trackpad Dragging -int 1 - - com.apple.dt.Xcode DVTTextAutoSuggestCompletionsDelay 0.1 # Xcode autocomplete delay - - com.apple.finder FXEnableExtensionChangeWarning -bool false # don't ask when changing file extension - - com.apple.finder FXPreferredViewStyle -string Nlsv # default to list view - - com.apple.finder QLEnableTextSelection -bool true # enable text selection in QuickLook - - com.apple.finder _FXShowPosixPathInTitle -bool true # show full path in Finder - - com.apple.screencapture disable-shadow -bool true # no window shadows when capturing windows - - com.apple.screencapture location ~/Downloads - - com.apple.screensaver askForPassword -int 1 - - - com.google.Chrome AppleEnableSwipeNavigateWithScrolls -bool false + # NSGlobalDomain defaults + - # don't quit idle applications + domain: -g + key: NSDisableAutomaticTermination + type: bool + value: true + - # subpixel rendering on non-Apple LCDs + key: AppleFontSmoothing + type: int + value: 2 + - # full keyboard access + key: AppleKeyboardUIMode + type: int + value: 3 + - # show all extensions by default + key: AppleShowAllExtensions + type: bool + value: true + - # keyboard repeat rate + key: KeyRepeat + type: int + value: 2 + - # delay before keyboard repeat + key: InitialKeyRepeat + type: int + value: 15 + - # set sidebar item size to small + key: NSTableViewDefaultSizeMode + type: int + value: 1 + - # disable resume + key: NSQuitAlwaysKeepsWindows + type: bool + value: false + - # add debug menu in web views + key: WebKitDeveloperExtras + type: bool + value: true + - # tap to click + key: com.apple.mouse.tapBehavior + type: bool + value: true + + - # enable Debug menu in Safari + domain: com.apple.Safari + key: IncludeInternalDebugMenu + type: bool + value: true + + - # disable dashboard + domain: com.apple.dashboard + key: mcx-disabled + type: bool + value: true + + - # don't write .DS_Store to network volumes + domain: com.apple.desktopservices + key: DSDontWriteNetworkStores + type: bool + value: true + + # Dock defaults + - # automatically hide and show the dock + domain: com.apple.dock + key: autohide + type: bool + value: true + - # minimize windows using the scale effect + domain: com.apple.dock + key: mineffect + type: string + value: scale + - # don't rearrange spaces + domain: com.apple.dock + key: mru-spaces + type: bool + value: false + - domain: com.apple.dock + key: orientation + type: string + value: left + - # set the bottom left hot corner to sleep the display + domain: com.apple.dock + key: wvous-bl-corner + type: int + value: 10 + + # Trackpad settings + - domain: com.apple.driver.AppleBluetoothMultitouch.trackpad + key: Clicking + type: bool + value: true + - domain: com.apple.driver.AppleBluetoothMultitouch.trackpad + key: DragLock + type: int + value: 1 + - domain: com.apple.driver.AppleBluetoothMultitouch.trackpad + key: Dragging + type: int + value: 1 + + # Finder defaults + - # don't ask when changing file extension + domain: com.apple.finder + key: FXEnableExtensionChangeWarning + type: bool + value: false + - # default to list view + domain: com.apple.finder + key: FXPreferredViewStyle + type: string + value: Nlsv + - # enable text selection in QuickLook + domain: com.apple.finder + key: QLEnableTextSelection + type: bool + value: true + - # show full path in Finder + domain: com.apple.finder + key: _FXShowPosixPathInTitle + type: bool + value: true + + - # no window shadows when capturing windows + domain: com.apple.screencapture + key: disable-shadow + type: bool + value: true + + - domain: com.apple.screencapture + key: location + type: string + value: ~/Downloads + + - domain: com.apple.screensaver + key: askForPassword + type: int + value: 1 + + - domain: com.google.Chrome + key: AppleEnableSwipeNavigateWithScrolls + type: bool + value: false