WIP: feature/43-add-support-for-completion-commands #44

Draft
glenux wants to merge 28 commits from feature/43-add-support-for-completion-commands into develop
30 changed files with 260 additions and 77 deletions

View file

@ -8,15 +8,20 @@
# List of patterns to ignore during preloading # List of patterns to ignore during preloading
ignore_list: ignore_list:
- ^\.git/
- ^lib.*
- ^doc/
- ^bin/ - ^bin/
- ^\.code_preloader.yml
- ^doc/
- ^\.drone.yml
- ^\.git/
- ^\.gitattributes
- ^\.gitignore
- ^lib.*
- ^LICENSES/
- ^_prompts/ - ^_prompts/
- ^\.reuse/ - ^\.reuse/
- ^LICENSES/
- ^\.vagrant/
- ^scripts/ - ^scripts/
- ^\.tool-versions
- ^\.vagrant/
# Path to the output file (if null, output to STDOUT) # Path to the output file (if null, output to STDOUT)
output_path: null output_path: null

View file

@ -24,7 +24,7 @@ test:
install: install:
install \ install \
-m 755 \ -m 755 \
bin/code-preloader \ bin/mfm \
$(PREFIX)/bin $(PREFIX)/bin
.PHONY: spec test build all prepare install .PHONY: spec test build all prepare install

View file

@ -14,6 +14,8 @@
> version of our project, please visit our primary repository at: > version of our project, please visit our primary repository at:
> <https://code.apps.glenux.net/glenux/mfm>. > <https://code.apps.glenux.net/glenux/mfm>.
<!-- hello -->
# Minimalist Fuse Manager (MFM) # Minimalist Fuse Manager (MFM)
MFM is a Crystal-lang CLI designed to streamline the management of various FUSE filesystems, such as sshfs, gocryptfs, httpdirfs, and more. Through its user-friendly interface, users can effortlessly mount and unmount filesystems, get real-time filesystem status, and handle errors proficiently. MFM is a Crystal-lang CLI designed to streamline the management of various FUSE filesystems, such as sshfs, gocryptfs, httpdirfs, and more. Through its user-friendly interface, users can effortlessly mount and unmount filesystems, get real-time filesystem status, and handle errors proficiently.
@ -44,18 +46,23 @@ To build from source, you'll also need:
For Debian/Ubuntu you can use the following command: For Debian/Ubuntu you can use the following command:
```shell-session ```shell-session
$ sudo apt-get update && sudo apt-get install libpcre3-dev libevent-2.1-dev $ sudo apt-get update && sudo apt-get install libpcre3-dev libevent-2.1-dev make
``` ```
## Installation ## Installation
### 1. From Source ### 1. From Source
1. Clone or download the source code. To get started with MFM, ensure that you have the prerequisites installed on your system (see above).
2. Navigate to the source directory.
3. Run `shards install` to fetch dependencies. Then follow these steps to install:
4. Compile using `shards build`.
5. The compiled binary will be in the `bin` directory. git clone https://code.apps.glenux.net/glenux/mfm
cd mfm
make prepare
make build
sudo make install # either to install system-wide
make install PREFIX=$HOME/.local # or to install as a user
### 2. Binary Download ### 2. Binary Download

View file

@ -4,6 +4,10 @@ shards:
git: https://github.com/crystal-ameba/ameba.git git: https://github.com/crystal-ameba/ameba.git
version: 1.6.1 version: 1.6.1
baked_file_system:
git: https://github.com/schovi/baked_file_system.git
version: 0.10.0
crinja: crinja:
git: https://github.com/straight-shoota/crinja.git git: https://github.com/straight-shoota/crinja.git
version: 0.8.1 version: 0.8.1

View file

@ -26,6 +26,9 @@ dependencies:
github: hugopl/version_from_shard github: hugopl/version_from_shard
tablo: tablo:
github: hutou/tablo github: hutou/tablo
baked_file_system:
github: schovi/baked_file_system
version: 0.10.0
development_dependencies: development_dependencies:
ameba: ameba:

View file

@ -0,0 +1,17 @@
require "./abstract_command"
module GX::Commands
class CompletionAutodetect < AbstractCommand
def initialize(@config : GX::Config)
end
def execute
STDERR.puts "FIXME: Completion auto-detection isn't implemented yet. Please select one of the following: --bash or --zsh"
exit(0)
end
def self.handles_mode
GX::Types::Mode::CompletionAutodetect
end
end
end

View file

@ -0,0 +1,17 @@
require "./abstract_command"
module GX::Commands
class CompletionBash < AbstractCommand
def initialize(@config : GX::Config)
end
def execute
completion_bash = FileStorage.get("completion.bash")
STDOUT.puts completion_bash.gets_to_end
end
def self.handles_mode
GX::Types::Mode::CompletionBash
end
end
end

View file

@ -0,0 +1,17 @@
require "./abstract_command"
module GX::Commands
class CompletionZsh < AbstractCommand
def initialize(@config : GX::Config)
end
def execute
completion_bash = FileStorage.get("completion.zsh")
STDOUT.puts completion_bash.gets_to_end
end
def self.handles_mode
GX::Types::Mode::CompletionZsh
end
end
end

View file

@ -1,11 +1,36 @@
require "./abstract_command" require "./abstract_command"
require "../file_storage"
module GX::Commands module GX::Commands
class ConfigInit < AbstractCommand class ConfigInit < AbstractCommand
def initialize(config : GX::Config) # FIXME def initialize(@config : GX::Config)
end end
def execute def execute
config_dir = File.join(@config.home_dir, ".config", "mfm")
config_file_path = File.join(config_dir, "config.yml")
# Guard condition to exit if the configuration file already exists
if File.exists?(config_file_path)
puts "Configuration file already exists at #{config_file_path}. No action taken."
return
end
puts "Creating initial configuration file at #{config_file_path}"
# Ensure the configuration directory exists
FileUtils.mkdir_p(config_dir)
# Read the default configuration content from the baked file storage
default_config_content = FileStorage.get("sample.mfm.yaml")
# Write the default configuration to the target path
File.write(config_file_path, default_config_content)
puts "Configuration file created successfully."
rescue ex
STDERR.puts "Error creating the configuration file: #{ex.message}"
exit(1)
end end
def self.handles_mode def self.handles_mode

View file

@ -1,15 +0,0 @@
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

@ -2,7 +2,7 @@ require "./abstract_command"
module GX::Commands module GX::Commands
class GlobalConfig < AbstractCommand class GlobalConfig < AbstractCommand
def initialize(config : GX::Config) # FIXME def initialize(config : GX::Config)
end end
def execute def execute

View file

@ -2,7 +2,7 @@ require "./abstract_command"
module GX::Commands module GX::Commands
class GlobalHelp < AbstractCommand class GlobalHelp < AbstractCommand
def initialize(@config : GX::Config) # FIXME def initialize(@config : GX::Config)
end end
def execute def execute

View file

@ -5,7 +5,7 @@ module GX::Commands
class MappingMount < AbstractCommand class MappingMount < AbstractCommand
@file_system_manager : FileSystemManager @file_system_manager : FileSystemManager
def initialize(@config : GX::Config) # FIXME def initialize(@config : GX::Config)
@config.load_from_env @config.load_from_env
@config.load_from_file @config.load_from_file
@file_system_manager = FileSystemManager.new(@config) @file_system_manager = FileSystemManager.new(@config)

View file

@ -5,7 +5,7 @@ module GX::Commands
class MappingUmount < AbstractCommand class MappingUmount < AbstractCommand
@file_system_manager : FileSystemManager @file_system_manager : FileSystemManager
def initialize(@config : GX::Config) # FIXME def initialize(@config : GX::Config)
@config.load_from_env @config.load_from_env
@config.load_from_file @config.load_from_file
@file_system_manager = FileSystemManager.new(@config) @file_system_manager = FileSystemManager.new(@config)

8
src/file_storage.cr Normal file
View file

@ -0,0 +1,8 @@
require "baked_file_system"
class FileStorage
extend BakedFileSystem
bake_folder "../static"
end

View file

@ -89,7 +89,9 @@ module GX
end end
def choose_filesystem def choose_filesystem
names_display = {} of String => NamedTuple(filesystem: Models::AbstractFilesystemConfig, ansi_name: String) names_display = {} of String => NamedTuple(
filesystem: Models::AbstractFilesystemConfig,
ansi_name: String)
config_root = @config.root config_root = @config.root
return if config_root.nil? return if config_root.nil?
@ -114,16 +116,16 @@ module GX
end end
# # FIXME: feat: allow to sort by name or by filesystem # # FIXME: feat: allow to sort by name or by filesystem
sorted_values = names_display.values.sort_by { |item| item[:filesystem].name } sorted_values = names_display.values.sort_by!(&.[:filesystem].name)
result_filesystem_name = Utils::Fzf.run(sorted_values.map(&.[:ansi_name])).strip result_filesystem_name = Utils::Fzf.run(sorted_values.map(&.[:ansi_name])).strip
selected_filesystem = names_display[result_filesystem_name][:filesystem] selected_filesystem = names_display[result_filesystem_name][:filesystem]
puts ">> #{selected_filesystem.name}".colorize(:yellow) puts ">> #{selected_filesystem.name}".colorize(:yellow)
if !selected_filesystem if !selected_filesystem
STDERR.puts "Vault not found: #{selected_filesystem}.".colorize(:red) STDERR.puts "Mapping not found: #{selected_filesystem}.".colorize(:red)
return return
end end
return selected_filesystem selected_filesystem
end end
private def generate_display_name(filesystem : Models::AbstractFilesystemConfig) : String private def generate_display_name(filesystem : Models::AbstractFilesystemConfig) : String
@ -136,7 +138,7 @@ module GX
if ENV["DISPLAY"]? || ENV["WAYLAND_DISPLAY"]? if ENV["DISPLAY"]? || ENV["WAYLAND_DISPLAY"]?
return true return true
end end
return false false
end end
end end
end end

View file

@ -22,7 +22,7 @@ struct BaseFormat < Log::StaticFormatter
end end
Log.setup do |config| Log.setup do |config|
backend = Log::IOBackend.new(formatter: BaseFormat) backend = Log::IOBackend.new(io: STDERR, formatter: BaseFormat)
config.bind "*", Log::Severity::Info, backend config.bind "*", Log::Severity::Info, backend
if ENV["LOG_LEVEL"]? if ENV["LOG_LEVEL"]?

View file

@ -31,7 +31,7 @@ module GX::Models::Concerns
end end
end end
def _mount_wrapper(&block) : Nil def _mount_wrapper(&) : Nil
mount_point_safe = mount_point mount_point_safe = mount_point
return if mount_point_safe.nil? return if mount_point_safe.nil?
@ -46,7 +46,7 @@ module GX::Models::Concerns
if result_status.success? if result_status.success?
puts "Models #{name} is now available on #{mount_point_safe}".colorize(:green) puts "Models #{name} is now available on #{mount_point_safe}".colorize(:green)
else else
puts "Error mounting the vault".colorize(:red) puts "Error mounting the mapping".colorize(:red)
return return
end end
end end

View file

@ -32,7 +32,7 @@ module GX::Models
output: STDOUT, output: STDOUT,
error: STDERR error: STDERR
) )
return process.wait process.wait
end end
end end
end end

View file

@ -32,7 +32,7 @@ module GX::Models
output: STDOUT, output: STDOUT,
error: STDERR error: STDERR
) )
return process.wait process.wait
end end
end end
end end

View file

@ -39,7 +39,7 @@ module GX::Models
output: STDOUT, output: STDOUT,
error: STDERR error: STDERR
) )
return process.wait process.wait
end end
end end
end end

View file

@ -4,6 +4,7 @@ module GX::Parsers
class CompletionParser < AbstractParser class CompletionParser < AbstractParser
def build(parser, ancestors, config) def build(parser, ancestors, config)
breadcrumbs = ancestors + "completion" breadcrumbs = ancestors + "completion"
# config.mode = Types::Mode::CompletionAutodetect
parser.banner = Utils.usage_line( parser.banner = Utils.usage_line(
breadcrumbs, breadcrumbs,
@ -12,11 +13,13 @@ module GX::Parsers
) )
parser.separator("\nCompletion commands:") parser.separator("\nCompletion commands:")
parser.on("--bash", "Generate bash completion") do |flag| parser.on("--bash", "Generate bash completion") do |_|
config.mode = Types::Mode::CompletionBash
Log.info { "Set bash completion" } Log.info { "Set bash completion" }
end end
parser.on("--zsh", "Generate zsh completion") do |flag| parser.on("--zsh", "Generate zsh completion") do |_|
config.mode = Types::Mode::CompletionZsh
Log.info { "Set zsh completion" } Log.info { "Set zsh completion" }
end end

View file

@ -23,7 +23,7 @@ module GX::Parsers
parser.banner = Utils.usage_line(breadcrumbs + "init", "Create initial mfm configuration") parser.banner = Utils.usage_line(breadcrumbs + "init", "Create initial mfm configuration")
parser.separator("\nInit options") parser.separator("\nInit options")
parser.on("-p", "--path", "Set vault encrypted path") do |path| parser.on("-p", "--path", "Set mapping encrypted path") do |path|
config.config_init_options.try do |opts| config.config_init_options.try do |opts|
opts.path = path opts.path = path
end end

View file

@ -5,8 +5,10 @@ module GX::Parsers
class MappingParser < AbstractParser class MappingParser < AbstractParser
def build(parser, ancestors, config) def build(parser, ancestors, config)
breadcrumbs = ancestors + "mapping" breadcrumbs = ancestors + "mapping"
add_args = {name: "", path: ""} create_args = {name: "", path: ""}
delete_args = {name: ""} delete_args = {name: ""}
mount_args = {name: ""}
umount_args = {name: ""}
parser.banner = Utils.usage_line( parser.banner = Utils.usage_line(
breadcrumbs, breadcrumbs,
@ -18,50 +20,94 @@ module GX::Parsers
parser.on("list", "List mappings") do parser.on("list", "List mappings") do
config.mode = Types::Mode::MappingList config.mode = Types::Mode::MappingList
parser.separator(Utils.help_line(breadcrumbs + "list")) parser.separator(Utils.help_line(breadcrumbs + "list"))
# abort("FIXME: Not implemented")
end end
parser.on("create", "Create mapping") do parser.on("create", "Create mapping") do
config.mode = Types::Mode::MappingCreate config.mode = Types::Mode::MappingCreate
# pp parser
pp parser
parser.banner = Utils.usage_line(breadcrumbs + "create", "Create mapping", true) parser.banner = Utils.usage_line(breadcrumbs + "create", "Create mapping", true)
parser.separator("\nCreate options") parser.separator("\nCreate options")
parser.on("-n", "--name", "Set vault name") do |name| parser.on("-t", "--type TYPE", "Set filesystem type") do |type|
add_args = add_args.merge({name: name}) create_args = create_args.merge({type: type})
end end
parser.on("-p", "--path", "Set vault encrypted path") do |path| parser.on("-n", "--name", "Set mapping name") do |name|
add_args = add_args.merge({path: path}) create_args = create_args.merge({name: name})
end end
# Filesystem specific
parser.on("--encrypted-path PATH", "Set encrypted path (for gocryptfs)") do |path|
encrypted_path = path
end
parser.on("--remote-user USER", "Set SSH user (for sshfs)") do |user|
create_args = create_args.merge({remote_user: user})
end
parser.on("--remote-host HOST", "Set SSH host (for sshfs)") do |host|
create_args = create_args.merge({remote_host: host})
end
parser.on("--source-path PATH", "Set remote path (for sshfs)") do |path|
create_args = create_args.merge({remote_path: path})
end
parser.on("--remote-port PORT", "Set SSH port (for sshfs)") do |port|
create_args = create_args.merge({remote_port: port})
end
parser.on("--url URL", "Set URL (for httpdirfs)") do |url|
create_args = create_args.merge({url: url})
end
parser.separator(Utils.help_line(breadcrumbs + "create")) parser.separator(Utils.help_line(breadcrumbs + "create"))
end end
parser.on("edit", "Edit configuration") do |flag| parser.on("edit", "Edit configuration") do |_|
config.mode = Types::Mode::MappingEdit config.mode = Types::Mode::MappingEdit
parser.on("--remote-user USER", "Set SSH user") do |user|
create_args = create_args.merge({remote_user: user})
end
parser.on("--remote-host HOST", "Set SSH host") do |host|
create_args = create_args.merge({remote_host: host})
end
parser.on("--source-path PATH", "Set remote path") do |path|
create_args = create_args.merge({remote_path: path})
end
parser.separator(Utils.help_line(breadcrumbs + "edit")) parser.separator(Utils.help_line(breadcrumbs + "edit"))
# abort("FIXME: Not implemented") # abort("FIXME: Not implemented")
end end
parser.on("mount", "Mount mapping") do |flag| parser.on("mount", "Mount mapping") do |_|
config.mode = Types::Mode::MappingMount config.mode = Types::Mode::MappingMount
parser.banner = Utils.usage_line(breadcrumbs + "mount", "mount mapping", true)
parser.separator("\nMount options")
parser.on("-n", "--name", "Set mapping name") do |name|
mount_args = mount_args.merge({name: name})
end
parser.separator(Utils.help_line(breadcrumbs + "mount")) parser.separator(Utils.help_line(breadcrumbs + "mount"))
# abort("FIXME: Not implemented")
end end
parser.on("umount", "Umount mapping") do |flag| parser.on("umount", "Umount mapping") do |_|
config.mode = Types::Mode::MappingUmount config.mode = Types::Mode::MappingUmount
parser.banner = Utils.usage_line(breadcrumbs + "umount", "umount mapping", true)
parser.separator("\nUmount options")
parser.on("-n", "--name", "Set mapping name") do |name|
umount_args = umount_args.merge({name: name})
end
parser.separator(Utils.help_line(breadcrumbs + "umount")) parser.separator(Utils.help_line(breadcrumbs + "umount"))
# abort("FIXME: Not implemented")
end end
parser.on("delete", "Delete mapping") do parser.on("delete", "Delete mapping") do
config.mode = Types::Mode::MappingDelete config.mode = Types::Mode::MappingDelete
parser.banner = Utils.usage_line(breadcrumbs + "delete", "Delete mapping", true) parser.banner = Utils.usage_line(breadcrumbs + "delete", "delete mapping", true)
parser.separator("\nDelete options") parser.separator("\ndelete options")
parser.on("-n", "--name", "Set vault name") do |name| parser.on("-n", "--name", "Set mapping name") do |name|
delete_args = delete_args.merge({name: name}) delete_args = delete_args.merge({name: name})
end end
parser.separator(Utils.help_line(breadcrumbs + "delete")) parser.separator(Utils.help_line(breadcrumbs + "delete"))

View file

@ -21,24 +21,24 @@ module GX::Parsers
config.path = path config.path = path
end end
parser.on("-v", "--verbose", "Set more verbosity") do |flag| parser.on("-v", "--verbose", "Set more verbosity") do |_|
Log.info { "Verbosity enabled" } Log.info { "Verbosity enabled" }
config.verbose = true config.verbose = true
end end
parser.on("-o", "--open", "Automatically open directory after mount") do |flag| parser.on("-o", "--open", "Automatically open directory after mount") do |_|
Log.info { "Auto-open enabled" } Log.info { "Auto-open enabled" }
config.auto_open = true config.auto_open = true
end end
parser.on("--version", "Show version") do |flag| parser.on("--version", "Show version") do |_|
config.mode = Types::Mode::GlobalVersion config.mode = Types::Mode::GlobalVersion
end end
parser.on("-h", "--help", "Show this help") do |flag| parser.on("-h", "--help", "Show this help") do |_|
config.mode = Types::Mode::GlobalHelp config.mode = Types::Mode::GlobalHelp
config.help_options = Parsers::Options::HelpOptions.new config.help_options = Parsers::Options::HelpOptions.new
config.help_options.try { |opts| opts.parser_snapshot = parser.dup } config.help_options.try(&.parser_snapshot=(parser.dup))
end end
parser.separator("\nGlobal commands:") parser.separator("\nGlobal commands:")
@ -46,7 +46,7 @@ module GX::Parsers
parser.on("config", "Manage configuration file") do parser.on("config", "Manage configuration file") do
config.mode = Types::Mode::GlobalHelp config.mode = Types::Mode::GlobalHelp
config.help_options = Parsers::Options::HelpOptions.new config.help_options = Parsers::Options::HelpOptions.new
config.help_options.try { |opts| opts.parser_snapshot = parser.dup } config.help_options.try(&.parser_snapshot=(parser.dup))
# config.command = Commands::Config.new(config) # config.command = Commands::Config.new(config)
Parsers::ConfigParser.new.build(parser, breadcrumbs, config) Parsers::ConfigParser.new.build(parser, breadcrumbs, config)
@ -59,7 +59,7 @@ module GX::Parsers
parser.on("mapping", "Manage mappings") do parser.on("mapping", "Manage mappings") do
config.mode = Types::Mode::GlobalHelp config.mode = Types::Mode::GlobalHelp
config.help_options = Parsers::Options::HelpOptions.new config.help_options = Parsers::Options::HelpOptions.new
config.help_options.try { |opts| opts.parser_snapshot = parser.dup } config.help_options.try(&.parser_snapshot=(parser.dup))
Parsers::MappingParser.new.build(parser, breadcrumbs, config) Parsers::MappingParser.new.build(parser, breadcrumbs, config)
end end
@ -69,7 +69,10 @@ module GX::Parsers
# end # end
parser.on("completion", "Manage completion") do parser.on("completion", "Manage completion") do
config.mode = Types::Mode::GlobalCompletion config.mode = Types::Mode::GlobalHelp
config.help_options = Parsers::Options::HelpOptions.new
config.help_options.try(&.parser_snapshot=(parser.dup))
Parsers::CompletionParser.new.build(parser, breadcrumbs, config) Parsers::CompletionParser.new.build(parser, breadcrumbs, config)
end end

View file

@ -4,11 +4,15 @@ module GX::Types
GlobalVersion GlobalVersion
GlobalHelp GlobalHelp
GlobalCompletion # GlobalCompletion
GlobalTui GlobalTui
GlobalConfig GlobalConfig
GlobalMapping GlobalMapping
CompletionBash
CompletionZsh
CompletionAutodetect
ConfigInit ConfigInit
MappingCreate MappingCreate

View file

@ -4,12 +4,12 @@ module GX::Utils
@ancestors = base @ancestors = base
end end
def +(elem : String) def +(other : String)
b = BreadCrumbs.new(@ancestors + [elem]) BreadCrumbs.new(@ancestors + [other])
end end
def to_s def to_s(io : IO)
@ancestors.join(" ") io << @ancestors.join(" ")
end end
def to_a def to_a

View file

@ -3,7 +3,7 @@ require "./breadcrumbs"
module GX::Utils module GX::Utils
def self.usage_line(breadcrumbs : BreadCrumbs, description : String, has_commands : Bool = false) def self.usage_line(breadcrumbs : BreadCrumbs, description : String, has_commands : Bool = false)
[ [
"Usage: #{breadcrumbs.to_s}#{has_commands ? " [commands]" : ""} [options]", "Usage: #{breadcrumbs}#{has_commands ? " [commands]" : ""} [options]",
"", "",
description, description,
"", "",
@ -12,6 +12,6 @@ module GX::Utils
end end
def self.help_line(breadcrumbs : BreadCrumbs) def self.help_line(breadcrumbs : BreadCrumbs)
"\nRun '#{breadcrumbs.to_s} COMMAND --help' for more information on a command." "\nRun '#{breadcrumbs} COMMAND --help' for more information on a command."
end end
end end

5
static/completion.zsh Normal file
View file

@ -0,0 +1,5 @@
#!/bin/zsh
# mfm Zsh completion script

32
static/sample.mfm.yaml Normal file
View file

@ -0,0 +1,32 @@
---
version: 1
global:
mount_point_base: "{{env.HOME}}/mnt"
filesystems:
##
## Sample configuration for encrypted vault (gocryptfs)
##
# - type: gocryptfs
# name: "Credential Vault"
# encrypted_path: "{{env.HOME}}/Documents/Credential.Vault"
#
##
## Sample configuration remote SSH directory (sshfs)
##
# - type: sshfs
# name: "Remote SSH server"
# remote_host: ssh.example.com
# remote_user: "{{env.USER}}"
# remote_path: "/home/{{env.USER}}"
# remote_port: 443
#
##
## Sample configuration for remote HTTP directory (httpdirfs)
##
- type: httpdirfs
name: "Debian Repository"
url: "http://ftp.debian.org/debian/"
# mount_point: "{{env.HOME}}/another.dir"
#