Compare commits

...

71 commits

Author SHA1 Message Date
3a8d9239b2 Merge pull request 'feat: add support for sshfs option (-o) in config' (#51) from feature/50-add-support-for-sshfs-options into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #51
2024-10-05 12:44:14 +00:00
5f775ac45f feat: add support for sshfs option (-o) in config
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-10-05 10:54:40 +02:00
37710103ec Update README.md
All checks were successful
continuous-integration/drone/push Build is passing
2024-05-14 07:33:09 +00:00
36fd938325 feat: add basic support for spec
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-15 02:06:28 +01:00
5b0655780e feat: add dependency upon tablo 2024-01-15 02:05:30 +01:00
f6f320e389 chore: ignore _* files/dirs 2024-01-15 02:05:07 +01:00
702f731d14 chore: improve code-preloader config 2024-01-15 02:04:42 +01:00
32f0b6832b feat: enable preview_mt flag 2024-01-15 02:04:25 +01:00
6feedc2c70 doc: update README according to code 2024-01-14 20:32:23 +01:00
9ef261779c refactor: introduce command design pattern 2024-01-14 20:31:38 +01:00
cbf39027c5 ci: bump crystal version
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-09 22:32:22 +01:00
642be92684 chore: add code-preloader config file 2024-01-07 19:46:09 +01:00
f279879ce0 chore: add test & install to Makefile 2024-01-07 19:45:39 +01:00
35a87cd7e0 feat: add basic support for bash completion 2024-01-07 17:47:11 +01:00
dd5aa1db6f doc: add preamble to README
All checks were successful
continuous-integration/drone/push Build is passing
2024-01-03 19:32:17 +01:00
0eda2a1003 Merge pull request 'feat: add defaut FZF options when none defined' (#34) from feature/29-add-default-display-options-for-fzf into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #34
2023-11-26 15:29:14 +00:00
3a30fd8a86 feat: add defaut FZF options when none defined
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-26 16:21:47 +01:00
9f3f3b24c1 fix: auto-open should not run on umounted filesystems
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-24 19:25:50 +01:00
041550cc0f fix: handle mount errors (with the right message) 2023-11-24 19:25:21 +01:00
fd9829c283 Merge branch 'develop' of code.apps.glenux.net:glenux/mfm into develop
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-24 17:04:32 +01:00
70b51527df doc: update example config to demonstrate templating 2023-11-24 17:02:17 +01:00
58e4ab05bf Update README.md
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-24 09:52:05 +00:00
d4c52cd044 Merge branch 'develop' of code.apps.glenux.net:glenux/mfm into develop
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-24 10:50:29 +01:00
32fea233d1 fix: rename global.mount_point to avoid misunderstanding
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-24 10:49:55 +01:00
84230a6828 feat: sort by fs.name instead of fs.type 2023-11-24 10:49:32 +01:00
211419ea02 chore: add watch+rebuild target 2023-11-24 10:48:55 +01:00
5107e80aa7 Update README.md
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-24 09:29:12 +00:00
7f789daefa Merge pull request 'Add option to auto-open directory after mount' (#33) from feature/30-add-option-to-auto-open-directory into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #33
2023-11-24 09:26:23 +00:00
cb14a04fbe feat: add support for auto-open option (-o, --open)
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-24 10:25:30 +01:00
63c0bbbb1c Merge pull request 'feature/6-add-configurable-global-mountpoint' (#32) from feature/6-add-configurable-global-mountpoint into develop
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Reviewed-on: #32
2023-11-24 08:26:27 +00:00
8fc9f2cfda Merge branch 'develop' into feature/6-add-configurable-global-mountpoint
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-24 08:09:46 +00:00
23d4def217 feat: implement local & global mount_point definition
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-24 00:20:16 +01:00
ee3f57ec20 refactor: define abstract defs & move most functions to concerns/base
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-21 23:11:21 +01:00
8efe8ea5d9 Merge branch 'develop' of code.apps.glenux.net:glenux/mfm into develop
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-21 00:33:47 +01:00
587bff04ca chore: pin crystal version with tool-versions 2023-11-21 00:33:37 +01:00
994f9e1885 refactor: use a better class hierarchy for filesystems
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-21 00:30:59 +01:00
8f2c2442a3 feat: add global.mountpoint and version parsing from YAML 2023-11-21 00:29:48 +01:00
d91e9a8fcd Add examples for templating & disable non-implemented parts.
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-20 15:45:40 +00:00
eb42b28841 Merge pull request 'fix: wrong comparison' (#28) from feature/16-handle-fusermount-u-return-codes into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #28
2023-11-20 11:40:39 +00:00
f05ee6b1b6 fix: wrong comparison
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-20 12:40:13 +01:00
dace7bf9f4 Merge pull request 'fix: handle 'fusermount -u' return codes' (#27) from feature/16-handle-fusermount-u-return-codes into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #27
2023-11-20 11:38:01 +00:00
283606c280 fix: handle 'fusermount -u' return codes
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-20 12:37:31 +01:00
bba30c5222 Merge tag 'v0.1.11' into develop
All checks were successful
continuous-integration/drone/push Build is passing
v0.1.11
2023-11-19 00:05:50 +01:00
5cfe6159c5 Merge branch 'release/v0.1.11'
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-19 00:05:45 +01:00
0b5531ba26 chore: bump version 2023-11-19 00:05:41 +01:00
ff87a1f953 Merge branch 'develop' of code.apps.glenux.net:glenux/mfm into develop
Some checks reported errors
continuous-integration/drone/push Build was killed
2023-11-19 00:05:08 +01:00
60a4730c75 Merge tag 'v0.1.10' into develop
v0.1.10
2023-11-19 00:04:21 +01:00
e9553e278b Merge branch 'release/v0.1.10' 2023-11-19 00:04:15 +01:00
713b92fac0 Merge pull request 'feat: extract version number from shard+git history' (#23) from feature/22-add-support-for-version-number-from-shard into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #23
2023-11-18 23:02:14 +00:00
46829bcc69 feat: extract version number from shard+git history
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-18 23:55:23 +01:00
3f22971fa8 Merge pull request 'fix: delay config loading' (#21) from feature/14-add-support-for-global-config-file into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #21
2023-11-18 22:36:20 +00:00
5e8c46dfcf fix: delay config loading
Some checks reported errors
continuous-integration/drone/push Build was killed
continuous-integration/drone/pr Build was killed
2023-11-18 23:35:54 +01:00
d9db2380d9 Merge pull request 'fix: add templating support in configuration' (#18) from feature/15-add-templating-support-in-configuration into develop
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: #18
2023-11-18 22:34:21 +00:00
3c4e4271b2 ci: switch to alpine for static linking
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/pr Build is failing
2023-11-18 22:32:22 +00:00
e6a9d01175 ci: bump crystal version 2023-11-18 22:32:22 +00:00
9f0902d91d ci: add missing dependencies 2023-11-18 22:32:22 +00:00
592f0fbe41 fix: add templating support in configuration
Refs: #15
2023-11-18 22:32:22 +00:00
576b7c62c6 Merge pull request 'fix: delay config file discovery' (#20) from feature/14-add-support-for-global-config-file into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #20
2023-11-18 22:31:21 +00:00
1c184a5557 fix: delay config file discovery
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2023-11-18 23:31:04 +01:00
2990e18b27 Merge pull request 'fix: add support for global config file' (#17) from feature/14-add-support-for-global-config-file into develop
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: #17
2023-11-18 19:06:05 +00:00
f94d0f1f39 fix: add support for global config file
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Refs: #14
2023-11-18 19:32:12 +01:00
47c9dbcd89 doc: add required dependencies & debian command
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-02 16:58:39 +01:00
afb97e0a89 fix: split vagrant provisioning into multiple parts 2023-11-02 16:55:29 +01:00
27508ed5ac feat: add support for version and verbose cli flags
All checks were successful
continuous-integration/drone/push Build is passing
2023-11-02 12:44:37 +01:00
9fc1ec3912 feat add support for configurable ssh port 2023-11-02 12:43:46 +01:00
48df4ccc79 feat: improve crossbuild scripts 2023-11-02 12:41:59 +01:00
5dca1d9f15 ci: add crossbuild scripts (drafts)
All checks were successful
continuous-integration/drone/push Build is passing
2023-10-26 14:40:39 +02:00
2d44f1fa77 ci: enable docker service to prepare for matrix builds
Some checks reported errors
continuous-integration/drone/push Build encountered an error
continuous-integration/drone Build is passing
2023-10-25 22:20:37 +02:00
71dbbb3941 doc: fix wrong license version
All checks were successful
continuous-integration/drone/push Build is passing
2023-10-25 16:38:20 +02:00
e3e27e9964 doc: in README, add link to binary releases
All checks were successful
continuous-integration/drone/push Build is passing
2023-10-25 16:35:24 +02:00
ba45f06b0a doc: fix README about commands
All checks were successful
continuous-integration/drone/push Build is passing
2023-10-25 16:34:24 +02:00
65 changed files with 1737 additions and 391 deletions

32
.code_preloader.yml Normal file
View file

@ -0,0 +1,32 @@
---
# Example configuration for Code-Preloader
# List of repository paths to preload
# source_list:
# - "path/to/repo1"
# - "path/to/repo2"
# List of patterns to ignore during preloading
ignore_list:
- ^\.git/
- ^lib.*
- ^doc/
- ^bin/
- ^_prompts/
- ^\.reuse/
- ^LICENSES/
- ^\.vagrant/
- ^scripts/
# Path to the output file (if null, output to STDOUT)
output_path: null
prompt:
# Optional: Path to a file containing the prompt header
header_path: null
# Optional: Path to a file containing the prompt footer
footer_path: null
# Optional: Path to a file container a jinja template to structure the prompt
template_path: null

View file

@ -5,7 +5,7 @@ name: default
steps:
- name: build:binary
image: crystallang/crystal:1.7.3
image: crystallang/crystal:1.11.0-alpine
environment:
PACKAGE_BASENAME: mfm_linux_amd64
volumes:
@ -13,11 +13,16 @@ steps:
path: /_cache
commands:
- pwd
- apt-get update &&
apt-get install -y cmake g++ libevent-dev libpcre3-dev libyaml-dev
# - |
# apt-get update && \
# apt-get install -y \
# cmake g++ \
# libevent-dev libpcre3-dev \
# libyaml-dev liblzma-dev
- shards install
- shards build --production --static
- strip bin/mfm
- ./bin/mfm --version
- mkdir -p /_cache/bin
- cp -r bin/mfm /_cache/bin/$PACKAGE_BASENAME
@ -74,8 +79,17 @@ steps:
# FIXME: handle multi-arch
# FIXME: publish only on tags
services:
- name: docker
image: docker:dind
privileged: true
volumes:
- name: dockersock
path: /var/run
volumes:
- name: cache
temp: {}
- name: dockersock
temp: {}
#

1
.gitignore vendored
View file

@ -3,6 +3,7 @@
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
/_*
.vagrant
bin
lib

1
.tool-versions Normal file
View file

@ -0,0 +1 @@
crystal 1.10.1

View file

@ -3,7 +3,29 @@
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
PREFIX=/usr
all: build
prepare:
shards install
build:
shards build
shards build --error-trace -Dpreview_mt
@echo SUCCESS
watch:
watchexec --restart --delay-run 3 -c -e cr make build
spec: test
test:
crystal spec --error-trace
install:
install \
-m 755 \
bin/code-preloader \
$(PREFIX)/bin
.PHONY: spec test build all prepare install

View file

@ -6,6 +6,13 @@
-->
[![Build Status](https://cicd.apps.glenux.net/api/badges/glenux/mfm/status.svg)](https://cicd.apps.glenux.net/glenux/mfm)
![License LGPL3.0-or-later](https://img.shields.io/badge/license-LGPL3.0--or--later-blue.svg)
[![Donate on patreon](https://img.shields.io/badge/patreon-donate-orange.svg)](https://patreon.com/glenux)
> :information_source: This project is available on our self-hosted server and
> on CodeBerg and GitHub as mirrors. For the latest updates and comprehensive
> version of our project, please visit our primary repository at:
> <https://code.apps.glenux.net/glenux/mfm>.
# Minimalist Fuse Manager (MFM)
@ -19,11 +26,27 @@ Before using MFM, make sure the following tools are installed on your system:
- **sshfs**: <https://github.com/libfuse/sshfs>
- **httpdirfs**: <https://github.com/fangfufu/httpdirfs>
- **fzf**: <https://github.com/junegunn/fzf>
- libpcre3
- libevent-2.1
For Debian/Ubuntu you can use the following command:
```shell-session
$ sudo apt-get update && sudo apt-get install libpcre3 libevent-2.1-7 fzf gocryptfs httpdirfs sshfs
```
## Building from source
To build from source, you'll also need:
- **crystal-lang**: <https://crystal-lang.org/>
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
```
## Installation
### 1. From Source
@ -36,23 +59,63 @@ To build from source, you'll also need:
### 2. Binary Download
Alternatively, download a pre-compiled binary version of MFM.
Alternatively, download [a pre-compiled binary
version](https://code.apps.glenux.net/glenux/mfm/releases) of MFM.
## Usage
### Command Line Options
Global
```
Usage: mfm [options]
Global options:
-c, --config FILE Specify configuration file
-h, --help Display this help
Global options
-c, --config FILE Set configuration file
-v, --verbose Set more verbosity
-o, --open Automatically open directory after mount
--version Show version
-h, --help Show this help
Commands:
create Add a new filesystem
delete Remove an existing filesystem
edit Modify the configuration
Commands (not implemented yet):
config Manage configuration file
mapping Manage filesystems
```
Config management
```
Usage: mfm filesystem [options]
Global options
-c, --config FILE Set configuration file
-v, --verbose Set more verbosity
-o, --open Automatically open directory after mount
--version Show version
-h, --help Show this help
Commands (not implemented yet):
init Create init file
```
Filesystem management
```
Usage: mfm mapping [options]
Global options
-c, --config FILE Set configuration file
-v, --verbose Set more verbosity
-o, --open Automatically open directory after mount
--version Show version
-h, --help Show this help
Commands (not implemented yet):
list List fuse mappings
create Create new fuse mapping
edit Edit fuse mapping
delete Create new fuse mapping
```
### Demo
@ -61,24 +124,26 @@ Commands:
## Configuration
MFM uses a YAML configuration file, typically found at `~/.config/mfm.yml`, to detail the filesystem names, types, and respective configurations.
MFM uses a YAML configuration file, typically found at `~/.config/mfm.yml`, to
detail the filesystem names, types, and respective configurations.
### YAML File Format
```yaml
---
version: "1"
global:
mountpoint: "/home/user/mnt/{{name}}"
mountpoint: "{{env.HOME}}/mnt"
filesystems:
- type: "gocryptfs"
name: "Work - SSH Keys"
encrypted_path: "/home/user/.ssh/keyring.work"
encrypted_path: "/home/user/.ssh/keyring.work.vault"
- type: "sshfs"
name: "Personal - Media Server"
remote_user: "user"
remote_user: "{{env.USER}}"
remote_host: "mediaserver.local"
remote_path: "/mnt/largedisk/music"
remote_port: 22
@ -86,7 +151,7 @@ filesystems:
- type: httpdirfs
name: "Debian Repository"
url: "http://ftp.debian.org/debian/"
# Add more filesystems as needed
```
@ -102,7 +167,7 @@ Contributing to MFM:
6. **Submit a Pull Request**: Begin a pull request to the main repository and explain your changes.
7. **Review**: Await feedback from the maintainers and respond as necessary.
By contributing, you agree to our code of conduct and GPL-2 license terms.
By contributing, you agree to our code of conduct and license terms.
## Authors and Contributors
@ -115,5 +180,5 @@ By contributing, you agree to our code of conduct and GPL-2 license terms.
## License
GNU GPL-3
GNU GPL-3

2
Vagrantfile vendored
View file

@ -30,5 +30,5 @@ Vagrant.configure('2') do |config|
machine.vm.network 'forwarded_port', guest: 80, host: 1080, host_ip: '127.0.0.1'
end
config.vm.provision 'shell', path: 'scripts/vagrant.provision.sh'
config.vm.provision 'shell', path: 'scripts/vagrant-provision/base.sh'
end

View file

@ -1,9 +1,8 @@
---
version: 1
global:
mountpoint: "~/mnt"
mountpoint: "{{env.HOME}}/mnt"
filesystems:
- type: gocryptfs
@ -16,7 +15,7 @@ filesystems:
- type: sshfs
name: "Personal - Remote Media Server"
remote_user: user
remote_user: "{{env.USER}}"
remote_host: mediaserver.local
remote_port: 22
remote_path: "/remote/path/to/media"

64
scripts/ci.crossbuild-alpine.sh Executable file
View file

@ -0,0 +1,64 @@
#!/bin/sh -eu
# vim: set ts=2 sw=2 et:
LOCAL_PROJECT_PATH="${1-$PWD}"
TARGET_ARCH="${2-amd64}"
DOCKER_IMAGE=""
BUILD_COMMAND=" \
shards build --static --release \
&& chown 1000:1000 -R bin \
&& find bin -type f -maxdepth 1 -exec mv {} {}_${TARGET_ARCH} \; \
"
INSTALL_CRYSTAL=" \
echo '@edge http://dl-cdn.alpinelinux.org/alpine/edge/community' >>/etc/apk/repositories \
&& apk add --update --no-cache --force-overwrite \
crystal@edge \
g++ \
gc-dev \
libxml2-dev \
llvm16-dev \
llvm16-static \
make \
musl-dev \
openssl-dev \
openssl-libs-static \
pcre-dev \
shards@edge \
yaml-dev \
yaml-static \
zlib-dev \
zlib-static \
"
# setup arch
case "$TARGET_ARCH" in
amd64) DOCKER_IMAGE="alpine" ;;
arm64) DOCKER_IMAGE="multiarch/alpine:aarch64-edge" ;;
armel) DOCKER_IMAGE="multiarch/alpine:armv7-edge" ;;
# armhf) DOCKER_IMAGE="multiarch/alpine:armhf-edge" ;;
# i386) DOCKER_IMAGE="multiarch/alpine:x86-edge" ;;
mips) DOCKER_IMAGE="multiarch/alpine:mips-edge" ;;
mipsel) DOCKER_IMAGE="multiarch/alpine:mipsel-edge" ;;
powerpc) DOCKER_IMAGE="multiarch/alpine:powerpc-edge" ;;
ppc64el) DOCKER_IMAGE="multiarch/alpine:ppc64el-edge" ;;
s390x) DOCKER_IMAGE="multiarch/alpine:s390x-edge" ;;
esac
# Compile Crystal project statically for target architecture
docker pull multiarch/qemu-user-static:register
docker run \
--rm \
--privileged \
multiarch/qemu-user-static:register \
--reset
docker run \
-it \
-v "$LOCAL_PROJECT_PATH:/app" \
-w /app \
--rm \
"$DOCKER_IMAGE" \
/bin/sh -c "$INSTALL_CRYSTAL && $BUILD_COMMAND"

82
scripts/ci.crossbuild-debian.sh Executable file
View file

@ -0,0 +1,82 @@
#!/bin/sh -eu
# vim: set ts=2 sw=2 et:
LOCAL_PROJECT_PATH="${1-$PWD}"
TARGET_ARCH="${2-amd64}"
DOCKER_IMAGE=""
BUILD_COMMAND=" \
shards build --static --release \
&& chown 1000:1000 -R bin \
&& find bin -type f -maxdepth 1 -exec mv {} {}_${TARGET_ARCH} \; \
"
# crystal
INSTALL_CRYSTAL=" \
sed -i -e 's/Types: deb/Types: deb deb-src/' /etc/apt/sources.list.d/debian.sources \
&& echo 'deb http://deb.debian.org/debian unstable main' > /etc/apt/sources.list.d/sid.list \
&& echo 'deb-src http://deb.debian.org/debian unstable main' >> /etc/apt/sources.list.d/sid.list \
&& apt-get update \
&& apt-get install -y \
g++ \
libxml2-dev \
llvm-dev \
make \
libssl-dev \
libpcre3-dev \
libyaml-dev \
zlib1g-dev \
dpkg-dev \
debuild \
&& apt source crystal \
&& apt build-dep crystal \
&& ls -lF \
&& debuild -b -uc -us \
"
# setup arch
case "$TARGET_ARCH" in
amd64) DOCKER_IMAGE="debian" ;;
arm64) DOCKER_IMAGE="arm64v8/debian" ;;
armel) DOCKER_IMAGE="arm32v7/debian" ;;
armhf) DOCKER_IMAGE="armhf/debian" ;;
i386) DOCKER_IMAGE="x86/debian" ;;
mips) DOCKER_IMAGE="mips/debian" ;;
mipsel) DOCKER_IMAGE="mipsel/debian" ;;
powerpc) DOCKER_IMAGE="powerpc/debian" ;;
ppc64el) DOCKER_IMAGE="ppc64el/debian" ;;
s390x) DOCKER_IMAGE="s390x/debian" ;;
esac
# Compile Crystal project statically for target architecture
docker pull multiarch/qemu-user-static
docker run \
--rm \
--privileged \
multiarch/qemu-user-static \
--reset -p yes
set -x
docker run \
-it \
-v "$LOCAL_PROJECT_PATH:/app" \
-w /app \
--rm \
--platform linux/arm64 \
"$DOCKER_IMAGE"
exit 0
set -x
docker run \
-it \
-v "$LOCAL_PROJECT_PATH:/app" \
-w /app \
--rm \
--platform linux/arm64 \
"$DOCKER_IMAGE" \
/bin/sh -c "$INSTALL_CRYSTAL && $BUILD_COMMAND"

View file

@ -0,0 +1,86 @@
#!/bin/sh -eu
# vim: set ts=2 sw=2 et:
LOCAL_PROJECT_PATH="${1-$PWD}"
TARGET_ARCH="${2-arm64}"
DOCKER_IMAGE=""
BUILD_COMMAND=" \
shards build --static --release \
&& chown 1000:1000 -R bin \
&& find bin -type f -maxdepth 1 -exec mv {} {}_${TARGET_ARCH} \; \
"
# crystal
INSTALL_CRYSTAL=" \
sed -i -e '/^deb/d' /etc/apt/sources.list \
&& sed -i -e '/jessie.updates/d' /etc/apt/sources.list \
&& sed -i -e 's/^# deb/deb/' /etc/apt/sources.list \
&& apt-get update"
cat > /dev/null <<EOF
"
&& apt-get install -y \
g\+\+ \
gcc \
curl \
autoconf \
automake \
python2 \
libxml2-dev \
llvm-dev \
make \
libssl-dev \
libpcre2-dev \
libyaml-dev \
zlib1g-dev \
"
EOF
# setup arch
case "$TARGET_ARCH" in
amd64) DOCKER_IMAGE="debian:8" ;;
arm64) DOCKER_IMAGE="arm64v8/debian:8" ;;
armel) DOCKER_IMAGE="arm32v7/debian" ;;
armhf) DOCKER_IMAGE="armhf/debian" ;;
i386) DOCKER_IMAGE="x86/debian" ;;
mips) DOCKER_IMAGE="mips/debian" ;;
mipsel) DOCKER_IMAGE="mipsel/debian" ;;
powerpc) DOCKER_IMAGE="powerpc/debian" ;;
ppc64el) DOCKER_IMAGE="ppc64el/debian" ;;
s390x) DOCKER_IMAGE="s390x/debian" ;;
esac
# Compile Crystal project statically for target architecture
docker pull multiarch/qemu-user-static
docker run \
--rm \
--privileged \
multiarch/qemu-user-static \
--reset -p yes
set -x
docker run \
-it \
-v "$LOCAL_PROJECT_PATH:/app" \
-w /app \
--rm \
--platform linux/arm64 \
"$DOCKER_IMAGE" \
/bin/sh -c "$INSTALL_CRYSTAL && bash"
exit 0
set -x
docker run \
-it \
-v "$LOCAL_PROJECT_PATH:/app" \
-w /app \
--rm \
--platform linux/arm64 \
"$DOCKER_IMAGE" \
/bin/sh -c "$INSTALL_CRYSTAL && $BUILD_COMMAND"

View file

@ -0,0 +1,32 @@
#!/bin/sh
set -e
set -u
USER="$(test -d /vagrant && echo "vagrant" || echo "debian")"
HOSTNAME="$(hostname)"
export DEBIAN_FRONTEND=noninteractive
echo "Installing required system packages"
apt-get update --allow-releaseinfo-change
apt-get install -y \
apt-transport-https \
ca-certificates \
git \
curl \
wget \
vim \
gnupg2 \
software-properties-common
# echo "Installing mfm requirements"
# apt-get install -y \
# fzf \
# sshfs \
# httpdirfs \
# libyaml-0-2 \
# libyaml-dev \
# libpcre3-dev \
# libevent-dev

View file

@ -9,26 +9,6 @@ HOSTNAME="$(hostname)"
export DEBIAN_FRONTEND=noninteractive
echo "Installing required system packages"
apt-get update --allow-releaseinfo-change
apt-get install -y \
apt-transport-https \
ca-certificates \
git \
curl \
wget \
vim \
gnupg2 \
software-properties-common
echo "Installing recording requirements"
apt-get install -y \
tmux \
mdp \
bat \
asciinema \
termtosvg
echo "Installing mfm requirements"
apt-get install -y \
fzf \
@ -39,24 +19,6 @@ apt-get install -y \
libpcre3-dev \
libevent-dev
#!/bin/sh
set -e
set -u
USER="$(test -d /vagrant && echo "vagrant" || echo "debian")"
CLUSTERS_DIR=/home/$USER/clusters
# Installation de kompose
if [ ! -f /usr/local/bin/kompose ]; then
DL="$(mktemp)"
curl \
-L https://github.com/kubernetes/kompose/releases/download/v1.22.0/kompose-linux-amd64 \
-o "$DL"
chmod +x "$DL"
mv "$DL" /usr/local/bin/kompose
fi
# Installing asdf
su - "$USER" -c "git config --global advice.detachedHead false"
su - "$USER" -c "rm -rf ~/.asdf"

View file

@ -0,0 +1,22 @@
#!/bin/sh
# install crystal
set -e
set -u
USER="$(test -d /vagrant && echo "vagrant" || echo "debian")"
HOSTNAME="$(hostname)"
export DEBIAN_FRONTEND=noninteractive
echo "Installing required system packages"
apt-get update --allow-releaseinfo-change
echo "Installing recording requirements"
apt-get install -y \
tmux \
mdp \
bat \
asciinema \
termtosvg

View file

@ -1,6 +1,22 @@
version: 2.0
shards:
ameba:
git: https://github.com/crystal-ameba/ameba.git
version: 1.6.1
crinja:
git: https://github.com/straight-shoota/crinja.git
version: 0.8.1
shellwords:
git: https://github.com/sztheory/shellwords-crystal.git
version: 0.1.0
tablo:
git: https://github.com/hutou/tablo.git
version: 0.10.1
version_from_shard:
git: https://github.com/hugopl/version_from_shard.git
version: 1.2.5

View file

@ -5,29 +5,30 @@
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
name: Minimalist FUSE Manager
version: 0.1.0
version: 0.2.0
targets:
mfm:
main: src/main.cr
# authors:
# - name <email@example.com>
authors:
- Glenn Y. Rolland <glenux@glenux.net>
# description: |
# Short description of gx-vault
description: |
FIXME. write description
dependencies:
crinja:
github: straight-shoota/crinja
shellwords:
github: szTheory/shellwords-crystal
version_from_shard:
github: hugopl/version_from_shard
tablo:
github: hutou/tablo
# dependencies:
# pg:
# github: will/crystal-pg
# version: "~> 0.5"
# development_dependencies:
# webmock:
# github: manastech/webmock.cr
development_dependencies:
ameba:
github: crystal-ameba/ameba
license: GPL-3

1
spec/spec_helper.cr Normal file
View file

@ -0,0 +1 @@
require "spec"

View file

@ -0,0 +1,46 @@
require "../spec_helper"
require "../../src/utils/breadcrumbs"
describe GX::Utils::BreadCrumbs do
context "Initialization" do
it "can initialize from array" do
# empty string
b1 = GX::Utils::BreadCrumbs.new([] of String)
b1.to_a.should be_empty
# simple string
b2 = GX::Utils::BreadCrumbs.new(["test1"])
b2.to_a.should eq(["test1"])
# array
b3 = GX::Utils::BreadCrumbs.new(["test1", "test2"])
b3.to_a.should eq(["test1", "test2"])
end
end
context "Functioning" do
it "can add values" do
# empty string
b1 = GX::Utils::BreadCrumbs.new([] of String)
b1.to_a.should be_empty
# simple string
b2 = b1 + "test1"
b2.to_a.should eq(["test1"])
b3 = b2 + "test2"
b3.to_a.should eq(["test1", "test2"])
end
it "can become a string" do
b1 = GX::Utils::BreadCrumbs.new([] of String)
b1.to_s.should eq("")
b2 = b1 + "test1"
b2.to_a.should eq("test1")
b3 = b2 + "test2"
b3.to_a.should eq("test1 test2")
end
end
end

View file

@ -5,104 +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|
@config.path = path
end
parser.on("-h", "--help", "Show this help") do |flag|
STDOUT.puts parser
exit(0)
end
parser.separator("\nCommands")
parser.on("create", "Create vault") do
@config.mode = Config::Mode::Add
parser.banner = "Usage: #{PROGRAM_NAME} create [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::Add
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::Edit
end
breadcrumbs = Utils::BreadCrumbs.new([] of String)
Parsers::RootParser.new.build(parser, breadcrumbs, @config)
end
pparser.parse(args)
end
def run()
@config.load_from_file
names_display = {} of String => NamedTuple(filesystem: Filesystem, ansi_name: String)
@config.filesystems.each do |filesystem|
fs_str = filesystem.type.ljust(12,' ')
result_name =
if filesystem.mounted?
"#{fs_str} #{filesystem.name} [open]"
else
"#{fs_str} #{filesystem.name}"
end
ansi_name =
if filesystem.mounted?
"#{fs_str.colorize(:dark_gray)} #{filesystem.name} [#{ "open".colorize(:green) }]"
else
"#{fs_str.colorize(:dark_gray)} #{filesystem.name}"
end
names_display[result_name] = {
filesystem: filesystem,
ansi_name: ansi_name
}
end
result_filesystem_name = Fzf.run(names_display.values.map(&.[:ansi_name]).sort)
selected_filesystem = names_display[result_filesystem_name][:filesystem]
puts ">> #{selected_filesystem.name}".colorize(:yellow)
if selected_filesystem
selected_filesystem.mounted? ? selected_filesystem.unmount : selected_filesystem.mount
else
STDERR.puts "Vault not found: #{selected_filesystem}.".colorize(:red)
end
def run
command = CommandFactory.create_command(@config, @config.mode)
abort("ERROR: unknown command for mode #{@config.mode}") if command.nil?
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

@ -3,61 +3,112 @@
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "./filesystems"
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
enum Mode
Add
Edit
Run
Log = ::Log.for("config")
class MissingFileError < Exception
end
record NoArgs
record AddArgs, name : String, path : String
record DelArgs, name : String
getter filesystems : Array(Filesystem)
# getter filesystems : Array(Models::AbstractFilesystemConfig)
getter home_dir : String
property mode : Mode
property path : String
getter root : Models::RootConfig?
property verbose : Bool
property mode : Types::Mode
property path : String?
property args : AddArgs.class | DelArgs.class | NoArgs.class
property auto_open : Bool
DEFAULT_CONFIG_PATH = "mfm.yml"
# 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()
if !ENV["HOME"]?
raise "Home directory not found"
end
def initialize
raise Models::InvalidEnvironmentError.new("Home directory not found") if !ENV["HOME"]?
@home_dir = ENV["HOME"]
@mode = Mode::Run
@filesystems = [] of Filesystem
@path = File.join(@home_dir, ".config", DEFAULT_CONFIG_PATH)
@verbose = false
@auto_open = false
@mode = Types::Mode::GlobalTui
@filesystems = [] of Models::AbstractFilesystemConfig
@path = nil
@args = NoArgs
end
def load_from_file
@filesystems = [] of Filesystem
private def detect_config_file
possible_files = [
File.join(@home_dir, ".config", "mfm", "config.yaml"),
File.join(@home_dir, ".config", "mfm", "config.yml"),
File.join(@home_dir, ".config", "mfm.yaml"),
File.join(@home_dir, ".config", "mfm.yml"),
File.join("/etc", "mfm", "config.yaml"),
File.join("/etc", "mfm", "config.yml"),
]
if !File.exists? @path
STDERR.puts "Error: file #{@path} does not exist!".colorize(:red)
exit(1)
possible_files.each do |file_path|
if File.exists?(file_path)
Log.info { "Configuration file found: #{file_path}" }
return file_path if File.exists?(file_path)
else
Log.debug { "Configuration file not found: #{file_path}" }
end
end
load_filesystems(@path)
Log.error { "No configuration file found in any of the standard locations" }
raise MissingFileError.new("Configuration file not found")
end
private def load_filesystems(config_path : String)
yaml_data = YAML.parse(File.read(config_path))
vaults_data = yaml_data["filesystems"].as_a
vaults_data.each do |filesystem_data|
type = filesystem_data["type"].as_s
name = filesystem_data["name"].as_s
# encrypted_path = filesystem_data["encrypted_path"].as_s
@filesystems << Filesystem.from_yaml(filesystem_data.to_yaml)
# @filesystems << Filesystem.new(name, encrypted_path, "#{name}.Open")
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
end
def load_from_file
config_path = @path
if config_path.nil?
config_path = detect_config_file()
end
@path = config_path
if !File.exists? config_path
Log.error { "File #{path} does not exist!".colorize(:red) }
exit(1)
end
file_data = File.read(config_path)
file_patched = Crinja.render(file_data, {"env" => ENV.to_h})
root = Models::RootConfig.from_yaml(file_patched)
mount_point_base_safe = root.global.mount_point_base
raise Models::InvalidMountpointError.new("Invalid global mount point") if mount_point_base_safe.nil?
root.filesystems.each do |selected_filesystem|
if !selected_filesystem.mount_point?
selected_filesystem.mount_point =
File.join(mount_point_base_safe, selected_filesystem.mounted_name)
end
end
@root = root
end
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

@ -1,9 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "./filesystems/gocryptfs"
require "./filesystems/sshfs"
require "./filesystems/httpdirfs"
require "./filesystems/filesystem"

View file

@ -1,42 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "yaml"
module GX
abstract class Filesystem
include YAML::Serializable
use_yaml_discriminator "type", {
gocryptfs: GoCryptFS,
sshfs: SshFS,
httpdirfs: HttpDirFS
}
property type : String
end
module GenericFilesystem
def unmount
system("fusermount -u #{mount_dir.shellescape}")
puts "Filesystem #{name} is now closed.".colorize(:green)
end
def mount(&block)
Dir.mkdir_p(mount_dir) unless Dir.exists?(mount_dir)
if mounted?
puts "Already mounted. Skipping.".colorize(:yellow)
return
end
yield
puts "Filesystem #{name} is now available on #{mount_dir}".colorize(:green)
end
end
end
require "./gocryptfs"
require "./sshfs"

View file

@ -1,47 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "shellwords"
require "./filesystem"
module GX
class GoCryptFS < Filesystem
getter name : String = ""
getter encrypted_path : String = ""
@[YAML::Field(key: "mount_dir", ignore: true)]
getter mount_dir : String = ""
include GenericFilesystem
def after_initialize()
home_dir = ENV["HOME"] || raise "Home directory not found"
@mount_dir = File.join(home_dir, "mnt/#{@name}.Open")
end
def mounted? : Bool
`mount`.includes?("#{encrypted_path} on #{mount_dir}")
end
def mount
super do
input = STDIN
output = STDOUT
error = STDERR
process = Process.new(
"gocryptfs",
["-idle", "15m", encrypted_path, mount_dir],
input: input,
output: output,
error: error
)
unless process.wait.success?
puts "Error mounting the vault".colorize(:red)
return
end
end
end
end
end

View file

@ -1,48 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "shellwords"
require "./filesystem"
module GX
class HttpDirFS < Filesystem
getter name : String = ""
getter url : String = ""
@[YAML::Field(key: "mount_dir", ignore: true)]
getter mount_dir : String = ""
include GenericFilesystem
def after_initialize()
home_dir = ENV["HOME"] || raise "Home directory not found"
@mount_dir = File.join(home_dir, "mnt/#{@name}")
end
def mounted? : Bool
`mount`.includes?("httpdirfs on #{mount_dir}")
end
def mount
super do
input = STDIN
output = STDOUT
error = STDERR
process = Process.new(
"httpdirfs",
["#{url}", mount_dir],
input: input,
output: output,
error: error
)
unless process.wait.success?
puts "Error mounting the filesystem".colorize(:red)
return
end
end
end
end
end

View file

@ -1,49 +0,0 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "shellwords"
require "./filesystem"
module GX
class SshFS < Filesystem
getter name : String = ""
getter remote_path : String = ""
getter remote_user : String = ""
getter remote_host : String = ""
@[YAML::Field(key: "mount_dir", ignore: true)]
getter mount_dir : String = ""
include GenericFilesystem
def after_initialize()
home_dir = ENV["HOME"] || raise "Home directory not found"
@mount_dir = File.join(home_dir, "mnt/#{@name}")
end
def mounted? : Bool
`mount`.includes?("#{remote_user}@#{remote_host}:#{remote_path} on #{mount_dir}")
end
def mount
super do
input = STDIN
output = STDOUT
error = STDERR
process = Process.new(
"sshfs",
["#{remote_user}@#{remote_host}:#{remote_path}", mount_dir],
input: input,
output: output,
error: error
)
unless process.wait.success?
puts "Error mounting the filesystem".colorize(:red)
return
end
end
end
end
end

View file

@ -6,13 +6,31 @@
require "yaml"
require "colorize"
require "json"
require "log"
require "./filesystems/gocryptfs"
require "./config"
require "./cli"
app = GX::Cli.new
app.parse_command_line(ARGV)
app.run
struct BaseFormat < Log::StaticFormatter
def run
string @entry.severity.label.downcase
string "("
source
string "): "
message
end
end
Log.setup do |config|
backend = Log::IOBackend.new(formatter: BaseFormat)
config.bind "*", Log::Severity::Info, backend
if ENV["LOG_LEVEL"]?
level = Log::Severity.parse(ENV["LOG_LEVEL"]) || Log::Severity::Info
config.bind "*", level, backend
end
end
cli = GX::Cli.new
cli.parse_command_line(ARGV)
cli.run

11
src/models.cr Normal file
View file

@ -0,0 +1,11 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "./models/root_config"
require "./models/global_config"
require "./models/gocryptfs_config"
require "./models/sshfs_config"
require "./models/httpdirfs_config"
require "./models/abstract_filesystem_config"

View file

@ -0,0 +1,38 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "yaml"
module GX::Models
class InvalidFilesystemError < Exception
end
class InvalidMountpointError < Exception
end
abstract class AbstractFilesystemConfig
include YAML::Serializable
# include YAML::Serializable::Strict
use_yaml_discriminator "type", {
gocryptfs: GoCryptFSConfig,
sshfs: SshFSConfig,
httpdirfs: HttpDirFSConfig,
}
getter type : String
getter name : String
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?
end
end

View file

@ -0,0 +1,54 @@
module GX::Models::Concerns
module Base
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
mount_point_safe = @mount_point
raise InvalidMountpointError.new("Invalid mountpoint value") if mount_point_safe.nil?
system("fusermount -u #{mount_point_safe.shellescape}")
fusermount_status = $?
if fusermount_status.success?
puts "Models #{name} is now closed.".colorize(:green)
else
puts "Error: Unable to unmount filesystem #{name} (exit code: #{fusermount_status.exit_code}).".colorize(:red)
end
end
def mount_point?
!mount_point.nil?
end
def mount
_mount_wrapper() do
_mount_action
end
end
def _mount_wrapper(&block) : Nil
mount_point_safe = mount_point
return if mount_point_safe.nil?
Dir.mkdir_p(mount_point_safe) unless Dir.exists?(mount_point_safe)
if mounted?
puts "Already mounted. Skipping.".colorize(:yellow)
return
end
result_status = yield
if result_status.success?
puts "Models #{name} is now available on #{mount_point_safe}".colorize(:green)
else
puts "Error mounting the vault".colorize(:red)
return
end
end
end
end

View file

@ -0,0 +1,30 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "yaml"
require "./abstract_filesystem_config"
module GX::Models
class InvalidEnvironmentError < Exception
end
class GlobalConfig
include YAML::Serializable
include YAML::Serializable::Strict
@[YAML::Field(key: "mount_point_base")]
getter mount_point_base : String?
def after_initialize
raise InvalidEnvironmentError.new("Home directory not found") if !ENV["HOME"]?
home_dir = ENV["HOME"]
# Set default mountpoint from global if none defined
if @mount_point_base.nil? || @mount_point_base.try &.empty?
@mount_point_base = File.join(home_dir, "mnt")
end
end
end
end

View file

@ -0,0 +1,38 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "shellwords"
require "./abstract_filesystem_config"
require "./concerns/base"
module GX::Models
class GoCryptFSConfig < AbstractFilesystemConfig
getter encrypted_path : String = ""
include Concerns::Base
def _mounted_prefix
"#{encrypted_path}"
end
def mounted_name
"#{@name}.Open"
end
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,
error: STDERR
)
return process.wait
end
end
end

View file

@ -0,0 +1,38 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "shellwords"
require "./abstract_filesystem_config"
require "./concerns/base"
module GX::Models
class HttpDirFSConfig < AbstractFilesystemConfig
getter url : String = ""
include Concerns::Base
def _mounted_prefix
"httpdirfs"
end
def mounted_name
@name
end
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,
error: STDERR
)
return process.wait
end
end
end

40
src/models/root_config.cr Normal file
View file

@ -0,0 +1,40 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "yaml"
require "./abstract_filesystem_config"
require "./global_config"
module GX::Models
# class CrinjaConverter
# 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})
# return value_patched
# end
# return "<null>"
# end
#
# def self.to_yaml(value, builder : YAML::Nodes::Builder)
# end
# end
class RootConfig
include YAML::Serializable
include YAML::Serializable::Strict
# @[YAML::Field(key: "version", converter: GX::Models::CrinjaConverter)]
@[YAML::Field(key: "version")]
getter version : String
@[YAML::Field(key: "global")]
getter global : GlobalConfig
@[YAML::Field(key: "filesystems")]
getter filesystems : Array(AbstractFilesystemConfig)
end
end

View file

@ -0,0 +1,53 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# SPDX-FileCopyrightText: 2023 Glenn Y. Rolland <glenux@glenux.net>
# Copyright © 2023 Glenn Y. Rolland <glenux@glenux.net>
require "shellwords"
require "./abstract_filesystem_config"
require "./concerns/base"
module GX::Models
class SshFSConfig < AbstractFilesystemConfig
getter remote_path : String = ""
getter remote_user : String = ""
getter remote_host : String = ""
getter remote_port : String = "22"
getter options : Array(String) = [] of String
include Concerns::Base
def _mounted_prefix
"#{@remote_user}@#{@remote_host}:#{@remote_path}"
end
def mounted_name
@name
end
def _mount_action
mount_point_safe = @mount_point
raise InvalidMountpointError.new("Invalid mount point") if mount_point_safe.nil?
options = [] of String
# merge sshfs options
@options.each do |option|
options.push("-o", option)
end
options.push("-p", remote_port)
options.push(
"#{@remote_user}@#{@remote_host}:#{@remote_path}",
mount_point_safe
)
process = Process.new(
"sshfs",
options,
input: STDIN,
output: STDOUT,
error: STDERR
)
return process.wait
end
end
end

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

5
src/version.cr Normal file
View file

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

45
static/completion.bash Normal file
View file

@ -0,0 +1,45 @@
#!/bin/bash
# mfm Bash completion script
_mfm() {
local cur prev opts
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
# Options globales pour 'mfm'
local mfm_global_opts="-c --config -v --verbose -o --open --version -h --help"
# Commandes pour 'mfm'
local mfm_cmds="config mapping completion"
# Options pour 'config' et 'mapping'
local config_opts="init"
local mapping_opts="list create edit mount umount delete"
# Ajouter les options globales à chaque cas
case "${prev}" in
mfm)
COMPREPLY=($(compgen -W "${mfm_cmds} ${mfm_global_opts}" -- ${cur}))
return 0
;;
config)
COMPREPLY=($(compgen -W "${config_opts} ${mfm_global_opts}" -- ${cur}))
return 0
;;
mapping)
COMPREPLY=($(compgen -W "${mapping_opts} ${mfm_global_opts}" -- ${cur}))
return 0
;;
*)
if [[ ${cur} == -* ]]; then
COMPREPLY=($(compgen -W "${mfm_global_opts}" -- ${cur}))
fi
return 0
;;
esac
}
# Appliquer la complétion à la fonction 'mfm'
complete -F _mfm mfm