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
ignore_list:
- ^\.git/
- ^lib.*
- ^doc/
- ^bin/
- ^\.code_preloader.yml
- ^doc/
- ^\.drone.yml
- ^\.git/
- ^\.gitattributes
- ^\.gitignore
- ^lib.*
- ^LICENSES/
- ^_prompts/
- ^\.reuse/
- ^LICENSES/
- ^\.vagrant/
- ^scripts/
- ^\.tool-versions
- ^\.vagrant/
# Path to the output file (if null, output to STDOUT)
output_path: null

View file

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

View file

@ -14,6 +14,8 @@
> version of our project, please visit our primary repository at:
> <https://code.apps.glenux.net/glenux/mfm>.
<!-- hello -->
# 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.
@ -44,18 +46,23 @@ To build from source, you'll also need:
For Debian/Ubuntu you can use the following command:
```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
### 1. From Source
1. Clone or download the source code.
2. Navigate to the source directory.
3. Run `shards install` to fetch dependencies.
4. Compile using `shards build`.
5. The compiled binary will be in the `bin` directory.
To get started with MFM, ensure that you have the prerequisites installed on your system (see above).
Then follow these steps to install:
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

View file

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

View file

@ -26,6 +26,9 @@ dependencies:
github: hugopl/version_from_shard
tablo:
github: hutou/tablo
baked_file_system:
github: schovi/baked_file_system
version: 0.10.0
development_dependencies:
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 "../file_storage"
module GX::Commands
class ConfigInit < AbstractCommand
def initialize(config : GX::Config) # FIXME
def initialize(@config : GX::Config)
end
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
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
class GlobalConfig < AbstractCommand
def initialize(config : GX::Config) # FIXME
def initialize(config : GX::Config)
end
def execute

View file

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

View file

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

View file

@ -5,7 +5,7 @@ module GX::Commands
class MappingUmount < AbstractCommand
@file_system_manager : FileSystemManager
def initialize(@config : GX::Config) # FIXME
def initialize(@config : GX::Config)
@config.load_from_env
@config.load_from_file
@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
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
return if config_root.nil?
@ -114,16 +116,16 @@ module GX
end
# # 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
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)
STDERR.puts "Mapping not found: #{selected_filesystem}.".colorize(:red)
return
end
return selected_filesystem
selected_filesystem
end
private def generate_display_name(filesystem : Models::AbstractFilesystemConfig) : String
@ -136,7 +138,7 @@ module GX
if ENV["DISPLAY"]? || ENV["WAYLAND_DISPLAY"]?
return true
end
return false
false
end
end
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ module GX::Parsers
class CompletionParser < AbstractParser
def build(parser, ancestors, config)
breadcrumbs = ancestors + "completion"
# config.mode = Types::Mode::CompletionAutodetect
parser.banner = Utils.usage_line(
breadcrumbs,
@ -12,11 +13,13 @@ module GX::Parsers
)
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" }
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" }
end

View file

@ -23,7 +23,7 @@ module GX::Parsers
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|
parser.on("-p", "--path", "Set mapping encrypted path") do |path|
config.config_init_options.try do |opts|
opts.path = path
end

View file

@ -5,8 +5,10 @@ module GX::Parsers
class MappingParser < AbstractParser
def build(parser, ancestors, config)
breadcrumbs = ancestors + "mapping"
add_args = {name: "", path: ""}
create_args = {name: "", path: ""}
delete_args = {name: ""}
mount_args = {name: ""}
umount_args = {name: ""}
parser.banner = Utils.usage_line(
breadcrumbs,
@ -18,50 +20,94 @@ module GX::Parsers
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
# 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})
parser.on("-t", "--type TYPE", "Set filesystem type") do |type|
create_args = create_args.merge({type: type})
end
parser.on("-p", "--path", "Set vault encrypted path") do |path|
add_args = add_args.merge({path: path})
parser.on("-n", "--name", "Set mapping name") do |name|
create_args = create_args.merge({name: name})
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"))
end
parser.on("edit", "Edit configuration") do |flag|
parser.on("edit", "Edit configuration") do |_|
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"))
# abort("FIXME: Not implemented")
end
parser.on("mount", "Mount mapping") do |flag|
parser.on("mount", "Mount mapping") do |_|
config.mode = Types::Mode::MappingMount
parser.separator(Utils.help_line(breadcrumbs + "mount"))
# abort("FIXME: Not implemented")
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.on("umount", "Umount mapping") do |flag|
parser.separator(Utils.help_line(breadcrumbs + "mount"))
end
parser.on("umount", "Umount mapping") do |_|
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"))
# 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.banner = Utils.usage_line(breadcrumbs + "delete", "delete mapping", true)
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})
end
parser.separator(Utils.help_line(breadcrumbs + "delete"))

View file

@ -21,24 +21,24 @@ module GX::Parsers
config.path = path
end
parser.on("-v", "--verbose", "Set more verbosity") do |flag|
parser.on("-v", "--verbose", "Set more verbosity") do |_|
Log.info { "Verbosity enabled" }
config.verbose = true
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" }
config.auto_open = true
end
parser.on("--version", "Show version") do |flag|
parser.on("--version", "Show version") do |_|
config.mode = Types::Mode::GlobalVersion
end
parser.on("-h", "--help", "Show this help") do |flag|
parser.on("-h", "--help", "Show this help") do |_|
config.mode = Types::Mode::GlobalHelp
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
parser.separator("\nGlobal commands:")
@ -46,7 +46,7 @@ module GX::Parsers
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.help_options.try(&.parser_snapshot=(parser.dup))
# config.command = Commands::Config.new(config)
Parsers::ConfigParser.new.build(parser, breadcrumbs, config)
@ -59,7 +59,7 @@ module GX::Parsers
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 }
config.help_options.try(&.parser_snapshot=(parser.dup))
Parsers::MappingParser.new.build(parser, breadcrumbs, config)
end
@ -69,7 +69,10 @@ module GX::Parsers
# end
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)
end

View file

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

View file

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

View file

@ -3,7 +3,7 @@ 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]",
"Usage: #{breadcrumbs}#{has_commands ? " [commands]" : ""} [options]",
"",
description,
"",
@ -12,6 +12,6 @@ module GX::Utils
end
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

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