1
0
mirror of https://github.com/wting/autojump synced 2024-10-27 20:34:07 +00:00
wting_autojump/bin/autojump
2013-12-17 12:04:11 -06:00

321 lines
9.6 KiB
Python
Executable File

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Copyright © 2008-2012 Joel Schaerer
Copyright © 2012-2013 William Ting
* This program 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, or (at your option)
any later version.
* This program 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 this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
from __future__ import division, print_function
from collections import namedtuple
from functools import partial
from itertools import ifilter
from itertools import imap
from math import sqrt
from operator import attrgetter
from operator import itemgetter
import os
import platform
import re
import sys
from argparse import ArgumentParser
from data import load
from data import save
from utils import decode
from utils import encode_local
from utils import first
from utils import has_uppercase
from utils import is_osx
from utils import print_entry
from utils import second
VERSION = 'release-v21.8.0'
Entry = namedtuple('Entry', ['path', 'weight'])
def set_defaults():
config = {}
config['tab_menu_separator'] = '__'
if is_osx():
data_home = os.path.join(
os.path.expanduser('~'),
'Library',
'autojump')
else:
data_home = os.getenv(
'XDG_DATA_HOME',
os.path.join(
os.path.expanduser('~'),
'.local',
'share',
'autojump'))
config['data_path'] = os.path.join(data_home, 'autojump.txt')
config['backup_path'] = os.path.join(data_home, 'autojump.txt.bak')
config['tmp_path'] = os.path.join(data_home, 'data.tmp')
return config
def parse_environment(config):
# TODO(ting|2013-12-16): add autojump_data_dir support
# TODO(ting|2013-12-15): add ignore case / smartcase support
# TODO(ting|2013-12-15): add symlink support
return config
def eval_arguments(config):
"""Evaluate arguments and run appropriate logic, returning an error code."""
parser = ArgumentParser(
description='Automatically jump to directory passed as an argument.',
epilog="Please see autojump(1) man pages for full documentation.")
parser.add_argument(
'directory', metavar='DIRECTORY', nargs='*', default='',
help='directory to jump to')
parser.add_argument(
'-a', '--add', metavar='DIRECTORY',
help='add path')
parser.add_argument(
'-i', '--increase', metavar='WEIGHT', nargs='?', type=int,
const=20, default=False,
help='increase current directory weight')
parser.add_argument(
'-d', '--decrease', metavar='WEIGHT', nargs='?', type=int,
const=15, default=False,
help='decrease current directory weight')
# parser.add_argument(
# '-b', '--bash', action="store_true", default=False,
# help='enclose directory quotes to prevent errors')
# parser.add_argument(
# '--complete', action="store_true", default=False,
# help='used for tab completion')
parser.add_argument(
'--purge', action="store_true", default=False,
help='remove non-existent paths from database')
parser.add_argument(
'-s', '--stat', action="store_true", default=False,
help='show database entries and their key weights')
parser.add_argument(
'-v', '--version', action="version", version="%(prog)s " +
VERSION, help='show version information')
args = parser.parse_args()
if args.add:
add_path(config, args.add)
return 0
if args.increase:
try:
print_entry(add_path(config, os.getcwdu(), args.increase))
return 0
except OSError:
print("Current directory no longer exists.", file=sys.stderr)
return 1
if args.decrease:
try:
print_entry(decrease_path(config, os.getcwdu(), args.decrease))
return 0
except OSError:
print("Current directory no longer exists.", file=sys.stderr)
return 1
if args.purge:
print("Purged %d entries." % purge_missing_paths(config))
return 0
if args.stat:
print_stats(config)
return 0
print(encode_local(find_matches(config, args.directory)))
return 0
# if args.complete:
# config['match_cnt'] = 9
# config['ignore_case'] = True
# config['args'] = args
return config
def add_path(config, path, increment=10):
"""
Add a new path or increment an existing one.
os.path.realpath() is not used because users prefer to have short, symlinked
paths with duplicate entries in the database than a single canonical path.
"""
path = decode(path).rstrip(os.sep)
if path == os.path.expanduser('~'):
return path, 0
data = load(config)
if path in data:
data[path] = sqrt((data[path]**2) + (increment**2))
else:
data[path] = increment
save(config, data)
return path, data[path]
def decrease_path(config, path, increment=15):
"""Decrease weight of existing path."""
path = decode(path).rstrip(os.sep)
data = load(config)
data[path] = max(0, data[path]-increment)
save(config, data)
return path, data[path]
def detect_smartcase(needles):
"""
If any needles contain an uppercase letter then use case sensitive
searching. Otherwise use case insensitive searching.
"""
return not any(imap(has_uppercase, needles))
def find_matches(config, needles, count=1):
"""Return [count] paths matching needles."""
entriefy = lambda tup: Entry(*tup)
data = sorted(
imap(entriefy, load(config).iteritems()),
key=attrgetter('weight'),
reverse=True)
if not needles:
return first(data).path
sanitize = lambda x: decode(x).rstrip(os.sep)
needles = map(sanitize, needles)
ignore_case = detect_smartcase(needles)
exact_matches = match_consecutive(needles, data, ignore_case)
exists = lambda entry: os.path.exists(entry.path)
result = first(ifilter(exists, exact_matches))
# TODO(ting|2013-12-17): remove debug print
print(result)
sys.exit(0)
return result.path if result else u''
def match_consecutive(needles, haystack, ignore_case=False):
"""
Matches consecutive needles at the end of a path.
For example:
needles = ['foo', 'baz']
haystack = [
(path="/foo/bar/baz", 10),
(path="/foo/baz/moo", 10),
(path="/moo/foo/baz", 10),
(path="/foo/baz", 10)]
regex needle = re.compile(r'''
foo # needle #1
[^/]* # all characters except os.sep zero or more times
/ # os.sep
[^/]* # all characters except os.sep zero or more times
baz # needle #2
[^/]* # all characters except os.sep zero or more times
$ # end of string
'''
result = [
(path="/moo/foo/baz", 10),
(path="/foo/baz", 10)]
"""
regex_no_sep = '[^' + os.sep + ']*'
regex_one_sep = regex_no_sep + os.sep + regex_no_sep
regex_no_sep_end = regex_no_sep + '$'
# can't use compiled regex because of flags
needle = regex_one_sep.join(needles) + regex_no_sep_end
regex_flags = re.IGNORECASE | re.UNICODE if ignore_case else re.UNICODE
found = lambda haystack: re.search(
needle,
haystack.path,
flags=regex_flags)
return ifilter(found, haystack)
def match_regex(needles, haystack, ignore_case=False):
"""
Performs an exact match by combining all arguments into a single regex
expression and finding matches.
For example:
needles = ['qui', 'fox']
regex needle = r'.*qui.*fox.*'
haystack = [
(path="foobar", 10.0),
(path="The quick brown fox jumped over the lazy dog", 12.3)]
result = [(path="The quick brown fox jumped over the lazy dog", 12.3)]
"""
regex_needle = '.*' + '.*'.join(needles) + '.*'
regex_flags = re.IGNORECASE | re.UNICODE if ignore_case else re.UNICODE
has_needle = lambda haystack: re.search(regex_needle, haystack.path, flags=regex_flags)
return ifilter(has_needle, haystack)
def purge_missing_paths(config):
"""Remove non-existent paths."""
exists = lambda x: os.path.exists(x[0])
old_data = load(config)
new_data = dict(ifilter(exists, old_data.iteritems()))
save(config, new_data)
return len(old_data) - len(new_data)
def print_stats(config):
data = load(config)
for path, weight in sorted(data.iteritems(), key=itemgetter(1)):
print_entry(path, weight)
print("________________________________________\n")
print("%d:\t total weight" % sum(data.itervalues()))
print("%d:\t number of entries" % len(data))
try:
print("%.2f:\t current directory weight" % data.get(os.getcwdu(), 0))
except OSError:
pass
print("\ndata:\t %s" % config['data_path'])
def main():
return eval_arguments(parse_environment(set_defaults()))
if __name__ == "__main__":
sys.exit(main())