From 9ef261779ce0ec5332fe34b5689a78835e0a738d Mon Sep 17 00:00:00 2001 From: Glenn Date: Sun, 14 Jan 2024 20:31:38 +0100 Subject: [PATCH] refactor: introduce command design pattern --- src/cli.cr | 184 ++------------------- src/command_factory.cr | 11 ++ src/commands.cr | 1 + src/commands/abstract_command.cr | 13 ++ src/commands/config_init.cr | 15 ++ src/commands/global_completion.cr | 15 ++ src/commands/global_config.cr | 15 ++ src/commands/global_help.cr | 18 ++ src/commands/global_mapping.cr | 16 ++ src/commands/global_tui.cr | 25 +++ src/commands/global_version.cr | 17 ++ src/commands/mapping_create.cr | 16 ++ src/commands/mapping_delete.cr | 16 ++ src/commands/mapping_edit.cr | 16 ++ src/commands/mapping_list.cr | 45 +++++ src/commands/mapping_mount.cr | 26 +++ src/commands/mapping_umount.cr | 24 +++ src/config.cr | 34 ++-- src/file_system_manager.cr | 142 ++++++++++++++++ src/main.cr | 2 - src/models/abstract_filesystem_config.cr | 20 +-- src/models/concerns/base.cr | 10 +- src/models/global_config.cr | 4 +- src/models/gocryptfs_config.cr | 14 +- src/models/httpdirfs_config.cr | 14 +- src/models/root_config.cr | 7 +- src/models/sshfs_config.cr | 18 +- src/parsers/base.cr | 5 + src/parsers/completion_parser.cr | 26 +++ src/parsers/config_parser.cr | 38 +++++ src/parsers/mapping_parser.cr | 73 ++++++++ src/parsers/options/config_init_options.cr | 7 + src/parsers/options/config_options.cr | 6 + src/parsers/options/help_options.cr | 7 + src/parsers/root_parser.cr | 92 +++++++++++ src/types/modes.cr | 21 +++ src/utils/breadcrumbs.cr | 19 +++ src/{ => utils}/fzf.cr | 6 +- src/utils/parser_lines.cr | 17 ++ src/version.cr | 3 - 40 files changed, 817 insertions(+), 241 deletions(-) create mode 100644 src/command_factory.cr create mode 100644 src/commands.cr create mode 100644 src/commands/abstract_command.cr create mode 100644 src/commands/config_init.cr create mode 100644 src/commands/global_completion.cr create mode 100644 src/commands/global_config.cr create mode 100644 src/commands/global_help.cr create mode 100644 src/commands/global_mapping.cr create mode 100644 src/commands/global_tui.cr create mode 100644 src/commands/global_version.cr create mode 100644 src/commands/mapping_create.cr create mode 100644 src/commands/mapping_delete.cr create mode 100644 src/commands/mapping_edit.cr create mode 100644 src/commands/mapping_list.cr create mode 100644 src/commands/mapping_mount.cr create mode 100644 src/commands/mapping_umount.cr create mode 100644 src/file_system_manager.cr create mode 100644 src/parsers/base.cr create mode 100644 src/parsers/completion_parser.cr create mode 100644 src/parsers/config_parser.cr create mode 100644 src/parsers/mapping_parser.cr create mode 100644 src/parsers/options/config_init_options.cr create mode 100644 src/parsers/options/config_options.cr create mode 100644 src/parsers/options/help_options.cr create mode 100644 src/parsers/root_parser.cr create mode 100644 src/types/modes.cr create mode 100644 src/utils/breadcrumbs.cr rename src/{ => utils}/fzf.cr (92%) create mode 100644 src/utils/parser_lines.cr diff --git a/src/cli.cr b/src/cli.cr index 631d390..4366d07 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -5,194 +5,38 @@ require "option_parser" require "./config" -require "./fzf" require "./version" +require "./parsers/root_parser" +require "./utils/breadcrumbs" +require "./utils/fzf" +require "./file_system_manager" +require "./command_factory" module GX class Cli Log = ::Log.for("cli") - @config : Config + @config : GX::Config - def initialize() + def initialize # Main execution starts here + # # FIXME: add a method to verify that FZF is installed @config = Config.new - - ## FIXME: check that FZF is installed end def parse_command_line(args) - # update - add_args = { name: "", path: "" } - delete_args = { name: "" } pparser = OptionParser.new do |parser| - parser.banner = "Usage: #{PROGRAM_NAME} [options]\n\nGlobal options" - - parser.on("-c", "--config FILE", "Set configuration file") do |path| - Log.info { "Configuration set to #{path}" } - @config.path = path - end - - parser.on("-v", "--verbose", "Set more verbosity") do |flag| - Log.info { "Verbosity enabled" } - @config.verbose = true - end - - parser.on("-o", "--open", "Automatically open directory after mount") do |flag| - Log.info { "Auto-open enabled" } - @config.auto_open = true - end - - parser.on("--version", "Show version") do |flag| - @config.mode = Config::Mode::ShowVersion - end - - parser.on("-h", "--help", "Show this help") do |flag| - STDOUT.puts parser - exit(0) - end - - parser.separator("\nCommands") - parser.on("config", "Manage configuration") do - parser.banner = "Usage: #{PROGRAM_NAME} config [commands] [options]\n\nGlobal options" - parser.separator("\nCommands") - - parser.on("create", "Create vault") do - @config.mode = Config::Mode::ConfigAdd - - parser.banner = "Usage: #{PROGRAM_NAME} config create [commands] [options]\n\nGlobal options" - parser.separator("\nCommand options") - - parser.on("-n", "--name", "Set vault name") do |name| - add_args = add_args.merge({ name: name }) - end - parser.on("-p", "--path", "Set vault encrypted path") do |path| - add_args = add_args.merge({ path: path }) - end - end - - parser.on("delete", "Delete vault") do - @config.mode = Config::Mode::ConfigAdd - - parser.banner = "Usage: #{PROGRAM_NAME} delete [options]\n\nGlobal options" - parser.separator("\nCommand options") - - parser.on("-n", "--name", "Set vault name") do |name| - delete_args = delete_args.merge({ name: name }) - end - end - - parser.on("edit", "Edit configuration") do |flag| - @config.mode = Config::Mode::ConfigEdit - end - end - + breadcrumbs = Utils::BreadCrumbs.new([] of String) + Parsers::RootParser.new.build(parser, breadcrumbs, @config) end pparser.parse(args) end - def run() - case @config.mode - when Config::Mode::ShowVersion - STDOUT.puts "#{PROGRAM_NAME} #{VERSION}" - when Config::Mode::Mount - @config.load_from_env - @config.load_from_file - filesystem = choose_filesystem - raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + def run + command = CommandFactory.create_command(@config, @config.mode) + abort("ERROR: unknown command for mode #{@config.mode}") if command.nil? - mount_or_umount(filesystem) - auto_open(filesystem) if filesystem.mounted? && @config.auto_open - end - end - - def auto_open(filesystem) - # FIXME: support xdg-open - # FIXME: support mailcap - # FIXME: support user-defined command - # FIXME: detect graphical environment - - mount_point_safe = filesystem.mount_point - raise Models::InvalidMountpointError.new("Invalid filesystem") if mount_point_safe.nil? - - if graphical_environment? - process = Process.new( - "xdg-open", ## FIXME: make configurable - [mount_point_safe], - input: STDIN, - output: STDOUT, - error: STDERR - ) - unless process.wait.success? - puts "Error opening filesystem".colorize(:red) - return - end - else - process = Process.new( - "vifm", ## FIXME: make configurable - [mount_point_safe], - input: STDIN, - output: STDOUT, - error: STDERR - ) - unless process.wait.success? - puts "Error opening filesystem".colorize(:red) - return - end - end - end - - def graphical_environment? - if ENV["DISPLAY"]? || ENV["WAYLAND_DISPLAY"]? - return true - end - return false - end - - def choose_filesystem() - names_display = {} of String => NamedTuple(filesystem: Models::AbstractFilesystemConfig, ansi_name: String) - - config_root = @config.root - return if config_root.nil? - - config_root.filesystems.each do |filesystem| - fs_str = filesystem.type.ljust(12,' ') - - suffix = "" - suffix_ansi = "" - if filesystem.mounted? - suffix = "[open]" - suffix_ansi = "[#{ "open".colorize(:green) }]" - end - - result_name = "#{fs_str} #{filesystem.name} #{suffix}".strip - ansi_name = "#{fs_str.colorize(:dark_gray)} #{filesystem.name} #{suffix_ansi}".strip - - names_display[result_name] = { - filesystem: filesystem, - ansi_name: ansi_name - } - end - - ## FIXME: feat: allow to sort by name or by filesystem - sorted_values = names_display.values.sort_by { |item| item[:filesystem].name } - result_filesystem_name = Fzf.run(sorted_values.map(&.[:ansi_name])).strip - selected_filesystem = names_display[result_filesystem_name][:filesystem] - puts ">> #{selected_filesystem.name}".colorize(:yellow) - - if !selected_filesystem - STDERR.puts "Vault not found: #{selected_filesystem}.".colorize(:red) - return - end - return selected_filesystem - end - - def mount_or_umount(selected_filesystem) - if !selected_filesystem.mounted? - selected_filesystem.mount() - else - selected_filesystem.umount() - end + command.try &.execute end end end diff --git a/src/command_factory.cr b/src/command_factory.cr new file mode 100644 index 0000000..c2d7e22 --- /dev/null +++ b/src/command_factory.cr @@ -0,0 +1,11 @@ +require "./commands" + +module GX + class CommandFactory + def self.create_command(config : GX::Config, mode : GX::Types::Mode) : Commands::AbstractCommand? + classes = {{ Commands::AbstractCommand.all_subclasses }} + command_klass = classes.find { |klass| klass.handles_mode == mode } + command_klass.try &.new(config) + end + end +end diff --git a/src/commands.cr b/src/commands.cr new file mode 100644 index 0000000..034a066 --- /dev/null +++ b/src/commands.cr @@ -0,0 +1 @@ +require "./commands/*" diff --git a/src/commands/abstract_command.cr b/src/commands/abstract_command.cr new file mode 100644 index 0000000..7591f15 --- /dev/null +++ b/src/commands/abstract_command.cr @@ -0,0 +1,13 @@ +require "../config" + +module GX::Commands + abstract class AbstractCommand + abstract def initialize(config : GX::Config) + + abstract def execute + + def self.mode + Gx::Types::Mode::None + end + end +end diff --git a/src/commands/config_init.cr b/src/commands/config_init.cr new file mode 100644 index 0000000..b9407d3 --- /dev/null +++ b/src/commands/config_init.cr @@ -0,0 +1,15 @@ +require "./abstract_command" + +module GX::Commands + class ConfigInit < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + end + + def self.handles_mode + GX::Types::Mode::ConfigInit + end + end +end diff --git a/src/commands/global_completion.cr b/src/commands/global_completion.cr new file mode 100644 index 0000000..d1be696 --- /dev/null +++ b/src/commands/global_completion.cr @@ -0,0 +1,15 @@ +require "./abstract_command" + +module GX::Commands + class GlobalCompletion < AbstractCommand + def initialize(@config : GX::Config) + end + + def execute + end + + def self.handles_mode + GX::Types::Mode::GlobalConfig + end + end +end diff --git a/src/commands/global_config.cr b/src/commands/global_config.cr new file mode 100644 index 0000000..c369556 --- /dev/null +++ b/src/commands/global_config.cr @@ -0,0 +1,15 @@ +require "./abstract_command" + +module GX::Commands + class GlobalConfig < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + end + + def self.handles_mode + GX::Types::Mode::GlobalConfig + end + end +end diff --git a/src/commands/global_help.cr b/src/commands/global_help.cr new file mode 100644 index 0000000..78f1616 --- /dev/null +++ b/src/commands/global_help.cr @@ -0,0 +1,18 @@ +require "./abstract_command" + +module GX::Commands + class GlobalHelp < AbstractCommand + def initialize(@config : GX::Config) # FIXME + end + + def execute + STDOUT.puts "" + @config.help_options.try { |opts| puts opts.parser_snapshot } + exit(0) + end + + def self.handles_mode + GX::Types::Mode::GlobalHelp + end + end +end diff --git a/src/commands/global_mapping.cr b/src/commands/global_mapping.cr new file mode 100644 index 0000000..0c61b95 --- /dev/null +++ b/src/commands/global_mapping.cr @@ -0,0 +1,16 @@ +require "./abstract_command" + +module GX::Commands + class GlobalMapping < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + # FIXME: implement + end + + def self.handles_mode + GX::Types::Mode::GlobalMapping + end + end +end diff --git a/src/commands/global_tui.cr b/src/commands/global_tui.cr new file mode 100644 index 0000000..58f22ed --- /dev/null +++ b/src/commands/global_tui.cr @@ -0,0 +1,25 @@ +require "./abstract_command" +require "../file_system_manager" + +module GX::Commands + class GlobalTui < AbstractCommand + @file_system_manager : FileSystemManager + + def initialize(@config : GX::Config) + @config.load_from_env + @config.load_from_file + @file_system_manager = FileSystemManager.new(@config) + end + + def execute + filesystem = @file_system_manager.choose_filesystem + raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + @file_system_manager.mount_or_umount(filesystem) + @file_system_manager.auto_open(filesystem) if filesystem.mounted? && @config.auto_open + end + + def self.handles_mode + GX::Types::Mode::GlobalTui + end + end +end diff --git a/src/commands/global_version.cr b/src/commands/global_version.cr new file mode 100644 index 0000000..9cd6757 --- /dev/null +++ b/src/commands/global_version.cr @@ -0,0 +1,17 @@ +require "./abstract_command" +require "../config" + +module GX::Commands + class GlobalVersion < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + STDOUT.puts "#{File.basename PROGRAM_NAME} #{VERSION}" + end + + def self.handles_mode + GX::Types::Mode::GlobalVersion + end + end +end diff --git a/src/commands/mapping_create.cr b/src/commands/mapping_create.cr new file mode 100644 index 0000000..1e3ec6f --- /dev/null +++ b/src/commands/mapping_create.cr @@ -0,0 +1,16 @@ +require "./abstract_command" + +module GX::Commands + class MappingCreate < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + # FIXME: implement + end + + def self.handles_mode + GX::Types::Mode::MappingCreate + end + end +end diff --git a/src/commands/mapping_delete.cr b/src/commands/mapping_delete.cr new file mode 100644 index 0000000..bed0534 --- /dev/null +++ b/src/commands/mapping_delete.cr @@ -0,0 +1,16 @@ +require "./abstract_command" + +module GX::Commands + class MappingDelete < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + # FIXME: implement + end + + def self.handles_mode + GX::Types::Mode::MappingDelete + end + end +end diff --git a/src/commands/mapping_edit.cr b/src/commands/mapping_edit.cr new file mode 100644 index 0000000..b0ef961 --- /dev/null +++ b/src/commands/mapping_edit.cr @@ -0,0 +1,16 @@ +require "./abstract_command" + +module GX::Commands + class MappingEdit < AbstractCommand + def initialize(config : GX::Config) # FIXME + end + + def execute + # FIXME: implement + end + + def self.handles_mode + GX::Types::Mode::MappingEdit + end + end +end diff --git a/src/commands/mapping_list.cr b/src/commands/mapping_list.cr new file mode 100644 index 0000000..35cd299 --- /dev/null +++ b/src/commands/mapping_list.cr @@ -0,0 +1,45 @@ +require "./abstract_command" +require "../file_system_manager" +require "tablo" + +module GX::Commands + class MappingList < AbstractCommand + def initialize(@config : GX::Config) + @config.load_from_env + @config.load_from_file + @file_system_manager = FileSystemManager.new(@config) + end + + def execute + filesystems = @config.root.try &.filesystems + return if filesystems.nil? + # pp filesystems + + fsdata = [] of Array(String) + filesystems.each do |item| + fsdata << [ + item.type, + item.name, + item.mounted?.to_s, + ] + end + # pp fsdata + + report = Tablo::Table.new( + fsdata, + # connectors: Tablo::CONNECTORS_SINGLE_ROUNDED + column_padding: 0, + style: "" # Tablo::STYLE_NO_MID_COL +) do |table| + table.add_column("TYPE") { |row| row[0] } + table.add_column("NAME", width: 40) { |row| row[1] } + table.add_column("MOUNTED") { |row| row[2] } + end + puts report + end + + def self.handles_mode + GX::Types::Mode::MappingList + end + end +end diff --git a/src/commands/mapping_mount.cr b/src/commands/mapping_mount.cr new file mode 100644 index 0000000..db81351 --- /dev/null +++ b/src/commands/mapping_mount.cr @@ -0,0 +1,26 @@ +require "./abstract_command" +require "../file_system_manager" + +module GX::Commands + class MappingMount < AbstractCommand + @file_system_manager : FileSystemManager + + def initialize(@config : GX::Config) # FIXME + @config.load_from_env + @config.load_from_file + @file_system_manager = FileSystemManager.new(@config) + end + + def execute + filesystem = @file_system_manager.choose_filesystem + raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + # @file_system_manager.mount_or_umount(filesystem) + filesystem.mount + @file_system_manager.auto_open(filesystem) if filesystem.mounted? && @config.auto_open + end + + def self.handles_mode + GX::Types::Mode::MappingMount + end + end +end diff --git a/src/commands/mapping_umount.cr b/src/commands/mapping_umount.cr new file mode 100644 index 0000000..dc61f1d --- /dev/null +++ b/src/commands/mapping_umount.cr @@ -0,0 +1,24 @@ +require "./abstract_command" +require "../file_system_manager" + +module GX::Commands + class MappingUmount < AbstractCommand + @file_system_manager : FileSystemManager + + def initialize(@config : GX::Config) # FIXME + @config.load_from_env + @config.load_from_file + @file_system_manager = FileSystemManager.new(@config) + end + + def execute + filesystem = @file_system_manager.choose_filesystem + raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + filesystem.umount + end + + def self.handles_mode + GX::Types::Mode::MappingUmount + end + end +end diff --git a/src/config.cr b/src/config.cr index bb0d0d7..d375cce 100644 --- a/src/config.cr +++ b/src/config.cr @@ -6,6 +6,11 @@ require "crinja" require "./models" +require "./types/modes" +require "./parsers/options/help_options" +require "./parsers/options/config_options" +require "./parsers/options/config_init_options" +require "./commands/abstract_command" module GX class Config @@ -14,14 +19,6 @@ module GX class MissingFileError < Exception end - enum Mode - ConfigAdd - ConfigDelete - ConfigEdit - ShowVersion - Mount - end - record NoArgs record AddArgs, name : String, path : String record DelArgs, name : String @@ -31,26 +28,31 @@ module GX getter root : Models::RootConfig? property verbose : Bool - property mode : Mode + property mode : Types::Mode property path : String? property args : AddArgs.class | DelArgs.class | NoArgs.class property auto_open : Bool - def initialize() + # FIXME: refactor and remove these parts from here + property help_options : Parsers::Options::HelpOptions? + property config_init_options : Parsers::Options::ConfigInitOptions? + property config_options : Parsers::Options::ConfigOptions? + + def initialize raise Models::InvalidEnvironmentError.new("Home directory not found") if !ENV["HOME"]? @home_dir = ENV["HOME"] @verbose = false @auto_open = false - @mode = Mode::Mount + @mode = Types::Mode::GlobalTui @filesystems = [] of Models::AbstractFilesystemConfig @path = nil @args = NoArgs end - private def detect_config_file() + private def detect_config_file possible_files = [ File.join(@home_dir, ".config", "mfm", "config.yaml"), File.join(@home_dir, ".config", "mfm", "config.yml"), @@ -73,8 +75,8 @@ module GX raise MissingFileError.new("Configuration file not found") end - def load_from_env() - if !ENV["FZF_DEFAULT_OPTS"]? + def load_from_env + if !ENV["FZF_DEFAULT_OPTS"]? # force defaults settings if none defined ENV["FZF_DEFAULT_OPTS"] = "--height 40% --layout=reverse --border" end @@ -93,7 +95,7 @@ module GX end file_data = File.read(config_path) - file_patched = Crinja.render(file_data, {"env" => ENV.to_h}) + file_patched = Crinja.render(file_data, {"env" => ENV.to_h}) root = Models::RootConfig.from_yaml(file_patched) @@ -102,7 +104,7 @@ module GX root.filesystems.each do |selected_filesystem| if !selected_filesystem.mount_point? - selected_filesystem.mount_point = + selected_filesystem.mount_point = File.join(mount_point_base_safe, selected_filesystem.mounted_name) end end diff --git a/src/file_system_manager.cr b/src/file_system_manager.cr new file mode 100644 index 0000000..91dc1a8 --- /dev/null +++ b/src/file_system_manager.cr @@ -0,0 +1,142 @@ +# require "./models/abstract_filesystem_config" +require "./utils/fzf" + +module GX + class FileSystemManager + Log = ::Log.for("file_system_manager") + + def initialize(@config : Config) + end + + # OBSOLETE: + # def mount_filesystem(filesystem : Models::AbstractFilesystemConfig) + # raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + # if filesystem.mounted? + # Log.info { "Filesystem already mounted." } + # return + # end + # filesystem.mount + # end + + # OBSOLETE: + # def umount_filesystem(filesystem : Models::AbstractFilesystemConfig) + # raise Models::InvalidFilesystemError.new("Invalid filesystem") if filesystem.nil? + # unless filesystem.mounted? + # Log.info { "Filesystem is not mounted." } + # return + # end + # filesystem.umount + # end + + def mount_or_umount(selected_filesystem) + if !selected_filesystem.mounted? + selected_filesystem.mount + else + selected_filesystem.umount + end + end + + def auto_open(filesystem) + # FIXME: detect xdg-open and use it if possible + # FIXME: detect mailcap and use it if no xdg-open found + # FIXME: support user-defined command in configuration + # FIXME: detect graphical environment + + mount_point_safe = filesystem.mount_point + raise Models::InvalidMountpointError.new("Invalid filesystem") if mount_point_safe.nil? + + if graphical_environment? + process = Process.new( + "xdg-open", # # FIXME: make configurable + [mount_point_safe], + input: STDIN, + output: STDOUT, + error: STDERR + ) + unless process.wait.success? + puts "Error opening filesystem".colorize(:red) + return + end + else + process = Process.new( + "vifm", # # FIXME: make configurable + [mount_point_safe], + input: STDIN, + output: STDOUT, + error: STDERR + ) + unless process.wait.success? + puts "Error opening filesystem".colorize(:red) + return + end + end + end + + def each(&) + config_root = @config.root + return if config_root.nil? + + config_root.filesystems.each do |filesystem| + yield filesystem + end + end + + def filesystems + config_root = @config.root + return if config_root.nil? + + config_root.filesystems + end + + def choose_filesystem + names_display = {} of String => NamedTuple(filesystem: Models::AbstractFilesystemConfig, ansi_name: String) + + config_root = @config.root + return if config_root.nil? + + config_root.filesystems.each do |filesystem| + fs_str = filesystem.type.ljust(12, ' ') + + suffix = "" + suffix_ansi = "" + if filesystem.mounted? + suffix = "[open]" + suffix_ansi = "[#{"open".colorize(:green)}]" + end + + result_name = "#{fs_str} #{filesystem.name} #{suffix}".strip + ansi_name = "#{fs_str.colorize(:dark_gray)} #{filesystem.name} #{suffix_ansi}".strip + + names_display[result_name] = { + filesystem: filesystem, + ansi_name: ansi_name, + } + end + + # # FIXME: feat: allow to sort by name or by filesystem + sorted_values = names_display.values.sort_by { |item| item[:filesystem].name } + result_filesystem_name = Utils::Fzf.run(sorted_values.map(&.[:ansi_name])).strip + selected_filesystem = names_display[result_filesystem_name][:filesystem] + puts ">> #{selected_filesystem.name}".colorize(:yellow) + + if !selected_filesystem + STDERR.puts "Vault not found: #{selected_filesystem}.".colorize(:red) + return + end + return selected_filesystem + end + + private def generate_display_name(filesystem : Models::AbstractFilesystemConfig) : String + fs_str = filesystem.type.ljust(12, ' ') + suffix = filesystem.mounted? ? "[open]" : "" + "#{fs_str} #{filesystem.name} #{suffix}".strip + end + + private def graphical_environment? + if ENV["DISPLAY"]? || ENV["WAYLAND_DISPLAY"]? + return true + end + return false + end + end +end diff --git a/src/main.cr b/src/main.cr index 2fc1327..28e57a4 100644 --- a/src/main.cr +++ b/src/main.cr @@ -34,5 +34,3 @@ end app = GX::Cli.new app.parse_command_line(ARGV) app.run - - diff --git a/src/models/abstract_filesystem_config.cr b/src/models/abstract_filesystem_config.cr index afa55bf..928a0d7 100644 --- a/src/models/abstract_filesystem_config.cr +++ b/src/models/abstract_filesystem_config.cr @@ -17,9 +17,9 @@ module GX::Models # include YAML::Serializable::Strict use_yaml_discriminator "type", { - gocryptfs: GoCryptFSConfig, - sshfs: SshFSConfig, - httpdirfs: HttpDirFSConfig + gocryptfs: GoCryptFSConfig, + sshfs: SshFSConfig, + httpdirfs: HttpDirFSConfig, } getter type : String @@ -27,12 +27,12 @@ module GX::Models property mount_point : String? abstract def _mount_wrapper(&block) - abstract def _mount_action() - abstract def _mounted_prefix() - abstract def mounted_name() - abstract def mounted?() - abstract def mount() - abstract def umount() - abstract def mount_point?() + abstract def _mount_action + abstract def _mounted_prefix + abstract def mounted_name + abstract def mounted? + abstract def mount + abstract def umount + abstract def mount_point? end end diff --git a/src/models/concerns/base.cr b/src/models/concerns/base.cr index a12fab2..90254de 100644 --- a/src/models/concerns/base.cr +++ b/src/models/concerns/base.cr @@ -1,14 +1,13 @@ - module GX::Models::Concerns module Base - def mounted?() : Bool + def mounted? : Bool mount_point_safe = @mount_point raise InvalidMountpointError.new("Invalid mountpoint value") if mount_point_safe.nil? `mount`.includes?(" on #{mount_point_safe} type ") end - def umount() : Nil + def umount : Nil mount_point_safe = @mount_point raise InvalidMountpointError.new("Invalid mountpoint value") if mount_point_safe.nil? @@ -22,11 +21,11 @@ module GX::Models::Concerns end end - def mount_point?() + def mount_point? !mount_point.nil? end - def mount() + def mount _mount_wrapper() do _mount_action end @@ -52,5 +51,4 @@ module GX::Models::Concerns end end end - end diff --git a/src/models/global_config.cr b/src/models/global_config.cr index 2e827a7..2cd57c1 100644 --- a/src/models/global_config.cr +++ b/src/models/global_config.cr @@ -17,7 +17,7 @@ module GX::Models @[YAML::Field(key: "mount_point_base")] getter mount_point_base : String? - def after_initialize() + def after_initialize raise InvalidEnvironmentError.new("Home directory not found") if !ENV["HOME"]? home_dir = ENV["HOME"] @@ -28,5 +28,3 @@ module GX::Models end end end - - diff --git a/src/models/gocryptfs_config.cr b/src/models/gocryptfs_config.cr index 3cbcd61..9013bdf 100644 --- a/src/models/gocryptfs_config.cr +++ b/src/models/gocryptfs_config.cr @@ -13,23 +13,23 @@ module GX::Models include Concerns::Base - def _mounted_prefix() + def _mounted_prefix "#{encrypted_path}" end - def mounted_name() + def mounted_name "#{@name}.Open" end - def _mount_action() + def _mount_action mount_point_safe = @mount_point raise InvalidMountpointError.new("Invalid mount point") if mount_point_safe.nil? process = Process.new( - "gocryptfs", - ["-idle", "15m", @encrypted_path, mount_point_safe], - input: STDIN, - output: STDOUT, + "gocryptfs", + ["-idle", "15m", @encrypted_path, mount_point_safe], + input: STDIN, + output: STDOUT, error: STDERR ) return process.wait diff --git a/src/models/httpdirfs_config.cr b/src/models/httpdirfs_config.cr index fdcf012..30db9eb 100644 --- a/src/models/httpdirfs_config.cr +++ b/src/models/httpdirfs_config.cr @@ -13,23 +13,23 @@ module GX::Models include Concerns::Base - def _mounted_prefix() + def _mounted_prefix "httpdirfs" end - def mounted_name() + def mounted_name @name end - def _mount_action() + def _mount_action mount_point_safe = @mount_point raise InvalidMountpointError.new("Invalid mount point") if mount_point_safe.nil? process = Process.new( - "httpdirfs", - ["#{@url}", mount_point_safe], - input: STDIN, - output: STDOUT, + "httpdirfs", + ["#{@url}", mount_point_safe], + input: STDIN, + output: STDOUT, error: STDERR ) return process.wait diff --git a/src/models/root_config.cr b/src/models/root_config.cr index c6de32b..d88dc61 100644 --- a/src/models/root_config.cr +++ b/src/models/root_config.cr @@ -12,13 +12,13 @@ module GX::Models # def self.from_yaml(ctx : YAML::ParseContext , node : YAML::Nodes::Node) # l_node = node # if l_node.is_a?(YAML::Nodes::Scalar) - # value_patched = Crinja.render(l_node.value, {"env" => ENV.to_h}) + # value_patched = Crinja.render(l_node.value, {"env" => ENV.to_h}) # return value_patched # end # return "" # end - # + # # def self.to_yaml(value, builder : YAML::Nodes::Builder) # end # end @@ -27,7 +27,7 @@ module GX::Models include YAML::Serializable include YAML::Serializable::Strict - # @[YAML::Field(key: "version", converter: GX::Models::CrinjaConverter)] + # @[YAML::Field(key: "version", converter: GX::Models::CrinjaConverter)] @[YAML::Field(key: "version")] getter version : String @@ -38,4 +38,3 @@ module GX::Models getter filesystems : Array(AbstractFilesystemConfig) end end - diff --git a/src/models/sshfs_config.cr b/src/models/sshfs_config.cr index fee42e5..99da441 100644 --- a/src/models/sshfs_config.cr +++ b/src/models/sshfs_config.cr @@ -16,27 +16,27 @@ module GX::Models include Concerns::Base - def _mounted_prefix() + def _mounted_prefix "#{@remote_user}@#{@remote_host}:#{@remote_path}" end - def mounted_name() + def mounted_name @name end - def _mount_action() + def _mount_action mount_point_safe = @mount_point raise InvalidMountpointError.new("Invalid mount point") if mount_point_safe.nil? process = Process.new( - "sshfs", + "sshfs", [ "-p", remote_port, - "#{@remote_user}@#{@remote_host}:#{@remote_path}", - mount_point_safe - ], - input: STDIN, - output: STDOUT, + "#{@remote_user}@#{@remote_host}:#{@remote_path}", + mount_point_safe, + ], + input: STDIN, + output: STDOUT, error: STDERR ) return process.wait diff --git a/src/parsers/base.cr b/src/parsers/base.cr new file mode 100644 index 0000000..8f3573b --- /dev/null +++ b/src/parsers/base.cr @@ -0,0 +1,5 @@ +module GX::Parsers + abstract class AbstractParser + abstract def build(parser : OptionParser, ancestors : BreadCrumbs, config : Config) + end +end diff --git a/src/parsers/completion_parser.cr b/src/parsers/completion_parser.cr new file mode 100644 index 0000000..1cdc4cc --- /dev/null +++ b/src/parsers/completion_parser.cr @@ -0,0 +1,26 @@ +require "./base.cr" + +module GX::Parsers + class CompletionParser < AbstractParser + def build(parser, ancestors, config) + breadcrumbs = ancestors + "completion" + + parser.banner = Utils.usage_line( + breadcrumbs, + "Manage #{PROGRAM_NAME} completion", + true + ) + parser.separator("\nCompletion commands:") + + parser.on("--bash", "Generate bash completion") do |flag| + Log.info { "Set bash completion" } + end + + parser.on("--zsh", "Generate zsh completion") do |flag| + Log.info { "Set zsh completion" } + end + + parser.separator Utils.help_line(breadcrumbs) + end + end +end diff --git a/src/parsers/config_parser.cr b/src/parsers/config_parser.cr new file mode 100644 index 0000000..b902cd5 --- /dev/null +++ b/src/parsers/config_parser.cr @@ -0,0 +1,38 @@ +require "./options/config_options" +require "./options/config_init_options" +require "./base" +require "../types/modes" +require "../utils/parser_lines" + +module GX::Parsers + class ConfigParser < AbstractParser + def build(parser, ancestors, config) + breadcrumbs = ancestors + "config" + config.config_options = Parsers::Options::ConfigOptions.new + parser.banner = Utils.usage_line( + breadcrumbs, + "Helpers for #{PROGRAM_NAME}'s configuration file", + true + ) + parser.separator("\nConfig commands") + + parser.on("init", "Create initial mfm configuration") do + config.mode = Types::Mode::ConfigInit + config.config_init_options = Parsers::Options::ConfigInitOptions.new + + parser.banner = Utils.usage_line(breadcrumbs + "init", "Create initial mfm configuration") + parser.separator("\nInit options") + + parser.on("-p", "--path", "Set vault encrypted path") do |path| + config.config_init_options.try do |opts| + opts.path = path + end + end + + parser.separator(Utils.help_line(breadcrumbs + "init")) + end + + parser.separator(Utils.help_line(breadcrumbs)) + end + end +end diff --git a/src/parsers/mapping_parser.cr b/src/parsers/mapping_parser.cr new file mode 100644 index 0000000..fc972a9 --- /dev/null +++ b/src/parsers/mapping_parser.cr @@ -0,0 +1,73 @@ +require "./base.cr" +require "../utils/parser_lines" + +module GX::Parsers + class MappingParser < AbstractParser + def build(parser, ancestors, config) + breadcrumbs = ancestors + "mapping" + add_args = {name: "", path: ""} + delete_args = {name: ""} + + parser.banner = Utils.usage_line( + breadcrumbs, + "Manage FUSE filesystem mappings", + true + ) + parser.separator("\nCommands") + + parser.on("list", "List mappings") do + config.mode = Types::Mode::MappingList + parser.separator(Utils.help_line(breadcrumbs + "list")) + # abort("FIXME: Not implemented") + end + + parser.on("create", "Create mapping") do + config.mode = Types::Mode::MappingCreate + + pp parser + parser.banner = Utils.usage_line(breadcrumbs + "create", "Create mapping", true) + parser.separator("\nCreate options") + + parser.on("-n", "--name", "Set vault name") do |name| + add_args = add_args.merge({name: name}) + end + parser.on("-p", "--path", "Set vault encrypted path") do |path| + add_args = add_args.merge({path: path}) + end + parser.separator(Utils.help_line(breadcrumbs + "create")) + end + + parser.on("edit", "Edit configuration") do |flag| + config.mode = Types::Mode::MappingEdit + parser.separator(Utils.help_line(breadcrumbs + "edit")) + # abort("FIXME: Not implemented") + end + + parser.on("mount", "Mount mapping") do |flag| + config.mode = Types::Mode::MappingMount + parser.separator(Utils.help_line(breadcrumbs + "mount")) + # abort("FIXME: Not implemented") + end + + parser.on("umount", "Umount mapping") do |flag| + config.mode = Types::Mode::MappingUmount + parser.separator(Utils.help_line(breadcrumbs + "umount")) + # abort("FIXME: Not implemented") + end + + parser.on("delete", "Delete mapping") do + config.mode = Types::Mode::MappingDelete + + parser.banner = Utils.usage_line(breadcrumbs + "delete", "Delete mapping", true) + parser.separator("\nDelete options") + + parser.on("-n", "--name", "Set vault name") do |name| + delete_args = delete_args.merge({name: name}) + end + parser.separator(Utils.help_line(breadcrumbs + "delete")) + end + + parser.separator Utils.help_line(breadcrumbs) + end + end +end diff --git a/src/parsers/options/config_init_options.cr b/src/parsers/options/config_init_options.cr new file mode 100644 index 0000000..97a31a3 --- /dev/null +++ b/src/parsers/options/config_init_options.cr @@ -0,0 +1,7 @@ +require "option_parser" + +module GX::Parsers::Options + class ConfigInitOptions + property path : String? + end +end diff --git a/src/parsers/options/config_options.cr b/src/parsers/options/config_options.cr new file mode 100644 index 0000000..6cacac0 --- /dev/null +++ b/src/parsers/options/config_options.cr @@ -0,0 +1,6 @@ +require "option_parser" + +module GX::Parsers::Options + class ConfigOptions + end +end diff --git a/src/parsers/options/help_options.cr b/src/parsers/options/help_options.cr new file mode 100644 index 0000000..e81fa2e --- /dev/null +++ b/src/parsers/options/help_options.cr @@ -0,0 +1,7 @@ +require "option_parser" + +module GX::Parsers::Options + class HelpOptions + property parser_snapshot : OptionParser? = nil + end +end diff --git a/src/parsers/root_parser.cr b/src/parsers/root_parser.cr new file mode 100644 index 0000000..70ac5a5 --- /dev/null +++ b/src/parsers/root_parser.cr @@ -0,0 +1,92 @@ +require "./base" +require "./config_parser" +require "./mapping_parser" +require "./completion_parser" +require "../utils/parser_lines" +require "../commands" + +module GX::Parsers + class RootParser < AbstractParser + def build(parser, ancestors, config) + breadcrumbs = ancestors + (File.basename PROGRAM_NAME) + + parser.banner = Utils.usage_line( + breadcrumbs, + "A management tool for your various FUSE filesystems", + true + ) + + parser.on("-c", "--config FILE", "Set configuration file") do |path| + Log.info { "Configuration set to #{path}" } + config.path = path + end + + parser.on("-v", "--verbose", "Set more verbosity") do |flag| + Log.info { "Verbosity enabled" } + config.verbose = true + end + + parser.on("-o", "--open", "Automatically open directory after mount") do |flag| + Log.info { "Auto-open enabled" } + config.auto_open = true + end + + parser.on("--version", "Show version") do |flag| + config.mode = Types::Mode::GlobalVersion + end + + parser.on("-h", "--help", "Show this help") do |flag| + config.mode = Types::Mode::GlobalHelp + config.help_options = Parsers::Options::HelpOptions.new + config.help_options.try { |opts| opts.parser_snapshot = parser.dup } + end + + parser.separator("\nGlobal commands:") + + parser.on("config", "Manage configuration file") do + config.mode = Types::Mode::GlobalHelp + config.help_options = Parsers::Options::HelpOptions.new + config.help_options.try { |opts| opts.parser_snapshot = parser.dup } + + # config.command = Commands::Config.new(config) + Parsers::ConfigParser.new.build(parser, breadcrumbs, config) + end + + parser.on("tui", "Interactive text user interface (default)") do + config.mode = Types::Mode::GlobalTui + end + + parser.on("mapping", "Manage mappings") do + config.mode = Types::Mode::GlobalHelp + config.help_options = Parsers::Options::HelpOptions.new + config.help_options.try { |opts| opts.parser_snapshot = parser.dup } + + Parsers::MappingParser.new.build(parser, breadcrumbs, config) + end + + # parser.on("interactive", "Interactive mapping mount/umount") do + # abort("FIXME: Not implemented") + # end + + parser.on("completion", "Manage completion") do + config.mode = Types::Mode::GlobalCompletion + Parsers::CompletionParser.new.build(parser, breadcrumbs, config) + end + + parser.separator(Utils.help_line(breadcrumbs)) + + # Manage errors + parser.unknown_args do |remaining_args, _| + next if remaining_args.size == 0 + + puts parser + abort("ERROR: Invalid arguments: #{remaining_args.join(" ")}") + end + + parser.invalid_option do |ex| + puts parser + abort("ERROR: Invalid option: '#{ex}'!") + end + end + end +end diff --git a/src/types/modes.cr b/src/types/modes.cr new file mode 100644 index 0000000..6498f89 --- /dev/null +++ b/src/types/modes.cr @@ -0,0 +1,21 @@ +module GX::Types + enum Mode + None + + GlobalVersion + GlobalHelp + GlobalCompletion + GlobalTui + GlobalConfig + GlobalMapping + + ConfigInit + + MappingCreate + MappingDelete + MappingEdit + MappingList + MappingMount + MappingUmount + end +end diff --git a/src/utils/breadcrumbs.cr b/src/utils/breadcrumbs.cr new file mode 100644 index 0000000..89c54e9 --- /dev/null +++ b/src/utils/breadcrumbs.cr @@ -0,0 +1,19 @@ +module GX::Utils + class BreadCrumbs + def initialize(base : Array(String)) + @ancestors = base + end + + def +(elem : String) + b = BreadCrumbs.new(@ancestors + [elem]) + end + + def to_s + @ancestors.join(" ") + end + + def to_a + @ancestors.clone + end + end +end diff --git a/src/fzf.cr b/src/utils/fzf.cr similarity index 92% rename from src/fzf.cr rename to src/utils/fzf.cr index bd1acc2..ff626f3 100644 --- a/src/fzf.cr +++ b/src/utils/fzf.cr @@ -3,9 +3,8 @@ # SPDX-FileCopyrightText: 2023 Glenn Y. Rolland # Copyright © 2023 Glenn Y. Rolland -module GX +module GX::Utils class Fzf - def self.run(list : Array(String)) : String input = IO::Memory.new input.puts list.join("\n") @@ -29,8 +28,7 @@ module GX exit(1) end - result = output.to_s.strip #.split.first? + result = output.to_s.strip # .split.first? end end end - diff --git a/src/utils/parser_lines.cr b/src/utils/parser_lines.cr new file mode 100644 index 0000000..dacd71f --- /dev/null +++ b/src/utils/parser_lines.cr @@ -0,0 +1,17 @@ +require "./breadcrumbs" + +module GX::Utils + def self.usage_line(breadcrumbs : BreadCrumbs, description : String, has_commands : Bool = false) + [ + "Usage: #{breadcrumbs.to_s}#{has_commands ? " [commands]" : ""} [options]", + "", + description, + "", + "Global options:", + ].join("\n") + end + + def self.help_line(breadcrumbs : BreadCrumbs) + "\nRun '#{breadcrumbs.to_s} COMMAND --help' for more information on a command." + end +end diff --git a/src/version.cr b/src/version.cr index 89bd308..0fd98ff 100644 --- a/src/version.cr +++ b/src/version.cr @@ -1,8 +1,5 @@ - require "version_from_shard" module GX VersionFromShard.declare end - -