dotfiler 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,36 @@
1
+ module Dotfiler
2
+ module CLI
3
+ module Commands
4
+ class Backup < Command
5
+ BACKUP_DIR = ".dotfiler_backup".freeze
6
+ TIMESTAMP_FORMAT = "%Y_%m_%d_%H_%M_%S".freeze
7
+
8
+ include Dotfiler::Import["copier", "remover"]
9
+
10
+ desc "Backup existing dotfiles directory"
11
+
12
+ def call(*)
13
+ handle_errors do
14
+ info("Backing up dotfiles directory (#{dotfiles_path}) to #{backup_dir_path}...")
15
+ copier.call(dotfiles_path, backup_dir_path)
16
+ remover.call(backup_dir_path.join(".git"), only_symlinks: false)
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def backup_dir_path
23
+ config.home_path.join("#{BACKUP_DIR}_#{current_timestamp}")
24
+ end
25
+
26
+ def current_timestamp
27
+ Time.now.strftime(TIMESTAMP_FORMAT)
28
+ end
29
+
30
+ def dotfiles_path
31
+ to_path.(config[:dotfiles])
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,39 @@
1
+ module Dotfiler
2
+ module CLI
3
+ module Commands
4
+ class Command < Hanami::CLI::Command
5
+ include Dotfiler::Import["config", "to_path", "shell"]
6
+
7
+ private
8
+
9
+ def handle_errors(&block)
10
+ begin
11
+ yield
12
+ rescue Dotfiler::Error => e
13
+ error!(e.message)
14
+ end
15
+ end
16
+
17
+ def print(*args)
18
+ shell.print(*args)
19
+ end
20
+
21
+ def info(message)
22
+ print(message, :info)
23
+ end
24
+
25
+ def error!(message)
26
+ terminate!(:error, message: message)
27
+ end
28
+
29
+ def terminate!(*args)
30
+ shell.terminate(*args)
31
+ end
32
+
33
+ def prompt(*args)
34
+ shell.prompt(*args)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ module Dotfiler
2
+ module CLI
3
+ module Commands
4
+ class Edit < Command
5
+ include Dotfiler::Import["fs", "dotfiles"]
6
+
7
+ desc "Edit specified dotfile. By default, dotfile will be opened in $EDITOR"
8
+
9
+ argument :name, required: true, desc: "Name of the dotfile you want to edit"
10
+ option :with, aliases: ["w"], required: false, desc: "Editor in which to open the specified dotfile"
11
+
12
+ def call(name:, **options)
13
+ handle_errors do
14
+ validate_name!(name)
15
+
16
+ dotfile_path = dotfiles.find(name).path.to_s
17
+ editor = options.fetch(:with, default_editor)
18
+
19
+ error!("Editor is not specified. Either set the '$EDITOR' environment variable or provide '--with' option") unless editor
20
+
21
+ fs.execute(editor, dotfile_path)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def validate_name!(name)
28
+ error!("Dotfile '#{name}' not found") unless dotfiles.exists?(name)
29
+ end
30
+
31
+ def default_editor
32
+ ENV["EDITOR"]
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,79 @@
1
+ module Dotfiler
2
+ module CLI
3
+ module Commands
4
+ class Init < Command
5
+ include Dotfiler::Import["fs"]
6
+
7
+ desc "Create config file. Create dotfiles directory at specified path. Initialize git repo."
8
+
9
+ argument :path, required: true, desc: "Path where the Dotfiles directory will be created"
10
+ option :git, type: :boolean, default: true, desc: "Initialize git repo for dotfiles directory"
11
+
12
+ def call(path:, **options)
13
+ handle_errors do
14
+ dotfiles_path = to_path.(path)
15
+
16
+ create_config_file(config_file_contents(dotfiles_path))
17
+ create_dotfiles_dir(dotfiles_path)
18
+ create_dotfiles_file(dotfiles_path)
19
+ initialize_vcs_repo(dotfiles_path) unless options[:git] == false
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def config_file_contents(dotfiles_path)
26
+ "dotfiles: #{dotfiles_path.to_s}"
27
+ end
28
+
29
+ def create_config_file(contents)
30
+ if config.file_path.exists?
31
+ answer = prompt("Config file (#{config.file_path}) already exists. Would you like to overwrite it?")
32
+
33
+ return unless answer == :yes
34
+ end
35
+
36
+ info("Creating config file (#{config.file_path})...")
37
+ fs.create_file(config.file_path.to_s, contents)
38
+ end
39
+
40
+ def create_dotfiles_dir(dotfiles_path)
41
+ if dotfiles_path.exists?
42
+ answer = prompt("Dotfiles directory (#{dotfiles_path}) already exists. Would you like to overwrite it?")
43
+
44
+ return unless answer == :yes
45
+
46
+ info("Removing existing dotfiles directory (#{dotfiles_path})...")
47
+ fs.remove(dotfiles_path.to_s)
48
+ end
49
+
50
+ info("Creating dotfiles directory (#{dotfiles_path})...")
51
+ fs.create_dir(dotfiles_path.to_s)
52
+ end
53
+
54
+ def create_dotfiles_file(dotfiles_path)
55
+ dotfiles_file = dotfiles_path.join(config.dotfiles_file_name)
56
+
57
+ if dotfiles_file.exists?
58
+ answer = prompt("Dotfiles file (#{dotfiles_file}) already exists. Would you like to overwrite it?")
59
+
60
+ return unless answer == :yes
61
+ end
62
+
63
+ info("Creating dotfiles file (#{dotfiles_file})...")
64
+ fs.create_file(dotfiles_file.to_s)
65
+ end
66
+
67
+ def initialize_vcs_repo(dotfiles_path)
68
+ if dotfiles_path.join(".git").exists?
69
+ answer = prompt("Dotfiles dir (#{dotfiles_path}) is already a git repository. Would you like to reinitialize it?")
70
+
71
+ return unless answer == :yes
72
+ end
73
+
74
+ info(fs.execute("git", "init", dotfiles_path.to_s, capture: true))
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,69 @@
1
+ module Dotfiler
2
+ module CLI
3
+ module Commands
4
+ class Install < Command
5
+ BACKUP_DIR = "dotfiler_installation_backup".freeze
6
+ TIMESTAMP_FORMAT = "%Y_%m_%d_%H_%M_%S".freeze
7
+
8
+ include Dotfiler::Import[ "fs", "dotfiles", "mover", "symlinker"]
9
+
10
+ desc "Install dotfiles from existing dotfiles directory"
11
+
12
+ argument :path, required: true, desc: "Path to existing dotfiles directory"
13
+
14
+ def call(path: , **options)
15
+ handle_errors do
16
+ dotfiles_path = to_path.(path)
17
+
18
+ validate_dotfiles_file!(dotfiles_path)
19
+
20
+ configure_dotfiles_dir(dotfiles_path)
21
+
22
+ config.reload!
23
+ dotfiles.config.reload!
24
+ dotfiles.reload!
25
+
26
+ create_symlinks
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def validate_dotfiles_file!(dotfiles_path)
33
+ dotfiles_path = dotfiles_path.join(config.dotfiles_file_name)
34
+
35
+ return if dotfiles_path.file?
36
+
37
+ error!("Dotfiles file (#{dotfiles_path}) not found...")
38
+ end
39
+
40
+ def configure_dotfiles_dir(dotfiles_path)
41
+ config.update!(dotfiles: dotfiles_path.to_s)
42
+ end
43
+
44
+ def create_symlinks
45
+ dotfiles.each do |dotfile|
46
+ backup(dotfile.link) if dotfile.link.exists? && !dotfile.link.symlink?
47
+
48
+ info("Symlinking dotfile (#{dotfile.path}) to #{dotfile.link}...")
49
+ symlinker.call(dotfile.path, dotfile.link, force: true)
50
+ end
51
+ end
52
+
53
+ def backup(path)
54
+ fs.create_dir(backup_dir_path.to_s) unless backup_dir_path.exists?
55
+ info("Backing up #{path} to #{backup_dir_path}...")
56
+ mover.call(path, backup_dir_path)
57
+ end
58
+
59
+ def backup_dir_path
60
+ @_backup_dir_path ||= config.home_path.join("#{BACKUP_DIR}_#{current_timestamp}")
61
+ end
62
+
63
+ def current_timestamp
64
+ Time.now.strftime(TIMESTAMP_FORMAT)
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,43 @@
1
+ module Dotfiler
2
+ module CLI
3
+ module Commands
4
+ class List < Command
5
+ include Dotfiler::Import["dotfiles"]
6
+
7
+ desc "List all managed dotfiles"
8
+
9
+ argument :names, desc: "List only names of managed dotfiles"
10
+
11
+ def call(names: nil)
12
+ handle_errors do
13
+ if dotfiles.list.empty?
14
+ terminate!(:info, message: "No dotfiles are managed at the moment")
15
+ else
16
+ content = generate_list(names_only: !names.nil?)
17
+ shell.print(content)
18
+ end
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def generate_list(names_only: false)
25
+ content = ""
26
+
27
+ dotfiles.each do |dotfile|
28
+ name, link, path = dotfile.to_h.values_at(:name, :link, :path)
29
+
30
+ content += " #{name}\n"
31
+
32
+ if !names_only
33
+ content += " - LINK: #{link}\n"
34
+ content += " - PATH: #{path}\n\n"
35
+ end
36
+ end
37
+
38
+ content
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,39 @@
1
+ module Dotfiler
2
+ module CLI
3
+ module Commands
4
+ class Remove < Command
5
+ include Dotfiler::Import["dotfiles", "mover", "remover"]
6
+
7
+ desc "Remove specified dotfile from dotfiles and restore it to it's original location"
8
+
9
+ argument :name, required: true, desc: "Name of the dotfile that should be unlinked"
10
+
11
+ def call(name:)
12
+ handle_errors do
13
+ validate_name!(name)
14
+
15
+ dotfile = dotfiles.find(name)
16
+
17
+ info("Removing symlink (#{dotfile.link})...")
18
+ remover.call(dotfile.link)
19
+
20
+ info("Restoring dotfile (#{dotfile.path}) to its original location (#{dotfile.link})...")
21
+ mover.call(dotfile.path, dotfile.link)
22
+
23
+
24
+ info("Removing '#{name}' from dotfiles...")
25
+ dotfiles.remove!(name)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def validate_name!(name)
32
+ return if dotfiles.exists?(name)
33
+
34
+ shell.terminate(:error, message: "Dotfile with the name '#{name}' does not exist")
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ module Dotfiler
2
+ module CLI
3
+ module Commands
4
+ class Version < Command
5
+ desc "Show version"
6
+
7
+ def call(*)
8
+ print("dotfiler #{Dotfiler::VERSION}")
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,71 @@
1
+ require "yaml"
2
+
3
+ module Dotfiler
4
+ class Config
5
+ CONFIG_FILE = ".dotfiler".freeze
6
+ DOTFILES_FILE = ".dotfiles".freeze
7
+ RELATIVE_HOME_PATH = "~".freeze
8
+
9
+ include Dotfiler::Import["fs", "to_path"]
10
+
11
+ def initialize(*args)
12
+ super
13
+ @data = load_data
14
+ end
15
+
16
+ def [](setting)
17
+ @data[setting]
18
+ end
19
+
20
+ def file_path
21
+ @_file_path ||= home_path.join(CONFIG_FILE)
22
+ end
23
+
24
+ def home_path
25
+ @_home_path ||= path(ENV["DOTFILER_HOME"] || Dir.home)
26
+ end
27
+
28
+ def relative_home_path
29
+ @_relative_home_path ||= path(RELATIVE_HOME_PATH, expand: false)
30
+ end
31
+
32
+ def set?
33
+ file_path.exists?
34
+ end
35
+
36
+ def dotfiles_file_path
37
+ path(self[:dotfiles]).join(dotfiles_file_name) if set?
38
+ end
39
+
40
+ def dotfiles_file_name
41
+ DOTFILES_FILE
42
+ end
43
+
44
+ def reload!
45
+ @data = load_data
46
+ end
47
+
48
+ def update!(args = {})
49
+ new_data = @data.merge(args)
50
+ file_content = new_data.each_with_object([]) { |(k, v), result| result << "#{k}: #{v}" }.join("\n")
51
+
52
+ fs.create_file(file_path.to_s, file_content)
53
+
54
+ reload!
55
+ end
56
+
57
+ private
58
+
59
+ def path(input, *opts)
60
+ to_path.(input, *opts)
61
+ end
62
+
63
+ def load_data
64
+ return {} unless set?
65
+
66
+ (YAML.load_file(file_path.to_s) || {}).each_with_object({}) do |(setting, value), result|
67
+ result[setting.to_sym] = value
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,58 @@
1
+ require "dry-container"
2
+ require "dry-auto_inject"
3
+
4
+ module Dotfiler
5
+ class Container
6
+ extend Dry::Container::Mixin
7
+
8
+ register "to_path", memoize: true do
9
+ Dotfiler::Path.method(:new)
10
+ end
11
+
12
+ register "fs", memoize: true do
13
+ Dotfiler::FileSystem.new
14
+ end
15
+
16
+ register "config", memoize: false do
17
+ Dotfiler::Config.new
18
+ end
19
+
20
+ register "dotfiles", memoize: false do
21
+ Dotfiler::Dotfiles.new
22
+ end
23
+
24
+ register "copier", memoize: true do
25
+ Dotfiler::Copier.new
26
+ end
27
+
28
+ register "mover", memoize: true do
29
+ Dotfiler::Mover.new
30
+ end
31
+
32
+ register "symlinker", memoize: true do
33
+ Dotfiler::Symlinker.new
34
+ end
35
+
36
+ register "remover", memoize: true do
37
+ Dotfiler::Remover.new
38
+ end
39
+
40
+ register "shell", memoize: true do
41
+ Dotfiler::Shell.new
42
+ end
43
+ end
44
+
45
+ Import = Dry::AutoInject(Container)
46
+ end
47
+
48
+ require "dotfiler/path"
49
+ require "dotfiler/file_system"
50
+ require "dotfiler/config"
51
+ require "dotfiler/dotfiles"
52
+
53
+ require "dotfiler/copier"
54
+ require "dotfiler/mover"
55
+ require "dotfiler/symlinker"
56
+ require "dotfiler/remover"
57
+
58
+ require "dotfiler/shell"