Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Break functionality into separate files
  • Loading branch information
jbernard committed Jan 17, 2016
1 parent 4d9988d commit 67e5e74
Show file tree
Hide file tree
Showing 7 changed files with 414 additions and 368 deletions.
126 changes: 126 additions & 0 deletions dotfiles/cli.py
@@ -0,0 +1,126 @@
import os
import py
import click

from .repository import Repository
from .exceptions import DotfileException


DEFAULT_HOMEDIR = os.path.expanduser('~/')
DEFAULT_REPO_PATH = os.path.expanduser('~/Dotfiles')
DEFAULT_REPO_IGNORE = ['.git']

pass_repo = click.make_pass_decorator(Repository)


@click.group(context_settings=dict(help_option_names=['-h', '--help']))
@click.option('-r', '--repository', type=click.Path(), show_default=True,
default=DEFAULT_REPO_PATH)
@click.version_option()
@click.pass_context
def cli(ctx, repository):
"""Dotfiles is a tool to make managing your dotfile symlinks in $HOME easy,
allowing you to keep all your dotfiles in a single directory.
"""
ctx.obj = Repository(py.path.local(repository),
py.path.local(DEFAULT_HOMEDIR),
DEFAULT_REPO_IGNORE)


@cli.command()
@click.option('-d', '--debug', is_flag=True,
help='Show commands that would be executed.')
@click.argument('files', nargs=-1, type=click.Path(exists=True))
@pass_repo
def add(repo, debug, files):
"""Replace file with symlink."""
for dotfile in repo.dotfiles(files):
dotfile.add(debug)

# try:
# repo.dotfile(py.path.local(filename)).add(debug)
# if not debug:
# click.echo('added \'%s\'' % filename)
# except DotfileException as err:
# click.echo(err)


@cli.command()
@click.option('-d', '--debug', is_flag=True,
help='Show commands that would be executed.')
@click.argument('files', nargs=-1, type=click.Path(exists=True))
@pass_repo
def remove(repo, debug, files):
"""Replace symlink with file."""
for filename in files:
try:
repo.dotfile(py.path.local(filename)).remove(debug)
if not debug:
click.echo('removed \'%s\'' % filename)
except DotfileException as err:
click.echo(err)


def show_dotfile(homedir, char, dotfile, color):
display_name = homedir.bestrelpath(dotfile.name)
click.secho('%c %s' % (char, display_name), fg=color)


@cli.command()
@click.option('-a', '--all', is_flag=True, help='Show all dotfiles.')
@click.option('-c', '--color', is_flag=True, help='Enable color output.')
@pass_repo
def status(repo, all, color):
"""Show all dotfiles in a non-OK state."""

state_info = {
'error': {'char': 'E', 'color': None},
'missing': {'char': '?', 'color': None},
'conflict': {'char': '!', 'color': None},
}

if all:
state_info['ok'] = {'char': ' ', 'color': None}

if color:
state_info['error']['color'] = 'red'
state_info['missing']['color'] = 'yellow'
state_info['conflict']['color'] = 'magenta'

for dotfile in repo.contents():
try:
char = state_info[dotfile.state]['char']
color = state_info[dotfile.state]['color']
show_dotfile(repo.homedir, char, dotfile, color)
except KeyError:
continue


@cli.command()
@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.')
@click.argument('files', nargs=-1, type=click.Path())
@pass_repo
def link(repo, verbose, files):
"""Create missing symlinks."""
# TODO: no files should be interpreted as all files with confirmation
for filename in files:
try:
repo.dotfile(py.path.local(filename)).link(verbose)
click.echo('linked \'%s\'' % filename)
except DotfileException as err:
click.echo(err)


@cli.command()
@click.option('-v', '--verbose', is_flag=True, help='Show executed commands.')
@click.argument('files', nargs=-1, type=click.Path(exists=True))
@pass_repo
def unlink(repo, verbose, files):
"""Remove existing symlinks."""
# TODO: no files should be interpreted as all files with confirmation
for filename in files:
try:
repo.dotfile(py.path.local(filename)).unlink(verbose)
click.echo('unlinked \'%s\'' % filename)
except DotfileException as err:
click.echo(err)
113 changes: 113 additions & 0 deletions dotfiles/dotfile.py
@@ -0,0 +1,113 @@
import py
from click import echo
from errno import EEXIST, ENOENT

from .exceptions import IsDirectory, IsSymlink, NotASymlink, DoesNotExist, \
TargetExists, TargetMissing


class Dotfile(object):
"""An configuration file managed within a repository.
:param name: name of the symlink in the home directory (~/.vimrc)
:param target: where the symlink should point to (~/Dotfiles/vimrc)
"""

def __init__(self, name, target):
self.name = name
self.target = target

def __str__(self):
return str(self.name)

def __repr__(self):
return '<Dotfile %r>' % self.name

def _ensure_subdirs(self, debug):
target_dir = py.path.local(self.target.dirname)
if not target_dir.check():
if debug:
echo('MKDIR %s' % self.target.dirname)
else:
target_dir.ensure_dir()

def _remove_subdirs(self, debug):
# TODO
pass

def _add(self, debug):
self._ensure_subdirs(debug)
if debug:
echo('MOVE %s -> %s' % (self.name, self.target))
else:
self.name.move(self.target)
self._link(debug)

def _remove(self, debug):
self._unlink(debug)
if debug:
echo('MOVE %s -> %s' % (self.target, self.name))
else:
self.target.move(self.name)
self._remove_subdirs(debug)

def _link(self, debug):
if debug:
echo('LINK %s -> %s' % (self.name, self.target))
else:
self.name.mksymlinkto(self.target, absolute=0)

def _unlink(self, debug):
if debug:
echo('UNLINK %s' % self.name)
else:
self.name.remove()

@property
def state(self):
if self.target.check(exists=0):
# only for testing, cli should never reach this state
return 'error'
elif self.name.check(exists=0):
# no $HOME symlink
return 'missing'
elif self.name.check(link=0) or not self.name.samefile(self.target):
# if name exists but isn't a link to the target
return 'conflict'
return 'ok'

def add(self, debug=False):
if self.name.check(file=0):
raise DoesNotExist(self.name)
if self.name.check(dir=1):
raise IsDirectory(self.name)
if self.name.check(link=1):
raise IsSymlink(self.name)
if self.target.check(exists=1):
raise TargetExists(self.name)
self._add(debug)

def remove(self, debug=False):
if not self.name.check(link=1):
raise NotASymlink(self.name)
if self.target.check(exists=0):
raise TargetMissing(self.name)
self._remove(debug)

# TODO: replace exceptions

def link(self, debug=False):
if self.name.check(exists=1):
raise OSError(EEXIST, self.name)
if self.target.check(exists=0):
raise OSError(ENOENT, self.target)
self._link(debug)

def unlink(self, debug=False):
if self.name.check(link=0):
raise Exception('%s is not a symlink' % self.name.basename)
if self.target.check(exists=0):
raise Exception('%s does not exist' % self.target)
if not self.name.samefile(self.target):
raise Exception('good lord')
self._unlink(debug)

0 comments on commit 67e5e74

Please sign in to comment.