Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Break functionality into separate files
- Loading branch information
Showing
7 changed files
with
414 additions
and
368 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) |
Oops, something went wrong.