refactor: introduce command design pattern

This commit is contained in:
Glenn Y. Rolland 2024-01-14 20:31:38 +01:00
parent cbf39027c5
commit 9ef261779c
40 changed files with 817 additions and 241 deletions

View file

@ -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

11
src/command_factory.cr Normal file
View file

@ -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

1
src/commands.cr Normal file
View file

@ -0,0 +1 @@
require "./commands/*"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

142
src/file_system_manager.cr Normal file
View file

@ -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

View file

@ -34,5 +34,3 @@ end
app = GX::Cli.new
app.parse_command_line(ARGV)
app.run

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 "<null>"
# 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

View file

@ -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

5
src/parsers/base.cr Normal file
View file

@ -0,0 +1,5 @@
module GX::Parsers
abstract class AbstractParser
abstract def build(parser : OptionParser, ancestors : BreadCrumbs, config : Config)
end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,7 @@
require "option_parser"
module GX::Parsers::Options
class ConfigInitOptions
property path : String?
end
end

View file

@ -0,0 +1,6 @@
require "option_parser"
module GX::Parsers::Options
class ConfigOptions
end
end

View file

@ -0,0 +1,7 @@
require "option_parser"
module GX::Parsers::Options
class HelpOptions
property parser_snapshot : OptionParser? = nil
end
end

View file

@ -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

21
src/types/modes.cr Normal file
View file

@ -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

19
src/utils/breadcrumbs.cr Normal file
View file

@ -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

View file

@ -3,9 +3,8 @@
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
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

17
src/utils/parser_lines.cr Normal file
View file

@ -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

View file

@ -1,8 +1,5 @@
require "version_from_shard"
module GX
VersionFromShard.declare
end