Skip to content
OpenDigitalCCPublic
forked from OpusVL/perl-ccfe

About

Clone of Curses Command Front End (CCFE) from ccfe.altervista.org.

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

CCFE — The Curses Command Front-end

CCFE is a small Perl/curses tool that puts an interactive, screen-oriented front-end on command-line scripts and commands: it prompts for the information a command needs and can be driven by your shell to provide predefined selections and run-time defaults.

Menus, forms and the commands they run are described in plain declarative text files, so you can build a whole guided interface — and ship it as a plugin — without writing any Perl.

The program source lives under src/; a mirror of the original project website and its documentation is under src/website-mirror/, and the manual pages are under src/man/.

Status

src/ccfe.pl is the program (currently version 2.4). The planning documents of the modernisation — packaging, security hardening, optional colour and a move to a modular, more functional structure — are kept in docs/history/. CCFE depends on standard packages only (Perl core plus libcurses-perl); the git history and its tags detail past fixes.

Recent work added things you drive from inside the running UI: a guided config wizard, runtime config reload, colour-theme and key-map switching (including Meta/Alt and Ctrl bindings), menu/form search, file-based notifications, object-ownership management, and a one-step "run CCFE at login" kiosk setup. These are covered below and in man ccfe.conf.

v2.0 reorganises the install layout (menus/forms moved out of lib/ into share/ccfe/objects/; per-user files follow the XDG base directories). See MIGRATION.md if you are upgrading from a 1.x install.

Dependencies

  • perl — core modules only (POSIX, IPC::Open3, Getopt::Std, Text::Balanced, File::Temp, …).
  • libcurses-perl — the Curses / ncurses + form + menu bindings.

On Debian/Ubuntu:

sudo apt-get install perl libcurses-perl

Install

From source. The installer copies the program unmodified (it resolves its paths at runtime from its own location) and installs the bundled sysmon sample plugin into the demo menu:

cd src
sh install.sh                       # interactive; or:
sh install.sh -b -p "$HOME/.ccfe"   # batch install under ~/.ccfe

Add the install's bin/ to your PATH (e.g. export PATH="$HOME/.ccfe/bin:$PATH") so you can just type ccfe. The tree under the prefix holds bin/, etc/, msg/, man/, doc/ and share/ccfe/{objects,themes,keymaps}.

From a package. The .deb, the .rpm, the Alpine package and the OpenWrt package (recipes in packaging/, built packages in dist/) all stage the same tree under /usr/lib/ccfe and link ccfe, ccfe-build, ccfe-gen and ccfe-login into /usr/bin and the man pages into the man path. The system configuration is a real config file, /etc/ccfe/ccfe.conf, which upgrades leave alone (/usr/lib/ccfe/etc/ccfe.conf is a link to it); the system menus and forms are in /usr/lib/ccfe/share/ccfe/objects/.

sudo apt install ./dist/ccfe_<version>_all.deb     # Debian, Ubuntu

Running CCFE

CCFE takes an optional shortcut naming the menu or form to open. With no argument it opens the program's default menu.

ccfe                 # default menu
ccfe demo            # the demo menu
ccfe sysmon          # the bundled sample-plugin menu

A shortcut can name a form as well as a menu, including one inside a .d/ form directory — handy for jumping straight to a single data-entry screen or wiring CCFE into another script:

ccfe demo.d/recursive    # open a form directly
ccfe sysmon.d/sar        # the sysmon "system activity report" form

CCFE resolves a shortcut <name> by looking for <name>.menu then <name>.form along its search path (see Plugins below). Run ccfe with no argument, or consult man ccfe, to see what is available in an installation.

One binary, many menu trees

CCFE keys its menu search path and config file off the name it is invoked as (basename($0)). Symlink the binary to a new name and it becomes a self-contained front-end with its own menus and its own <name>.conf:

ln -s "$HOME/.ccfe/bin/ccfe" "$HOME/.ccfe/bin/ops"
mkdir -p "$HOME/.local/share/ccfe/ops"   # drop ops's *.menu / *.form here
ops                                      # runs the "ops" tree, reads ops.conf

This makes CCFE a natural way to give a team (or a constrained login) a purpose-built menu without touching the default ccfe one.

An instance opens <name>.menu (or <name>.form) from its object path when there is one, and ccfe.menu otherwise. Its messages come from the default set unless a msg/<lang>/<name> file exists. theme = NAME in an instance's config finds <name>.conf.NAME in the themes directory first and then the shipped ccfe.conf.NAME, so an instance can use smit-color without a copy; the theme switcher lists both. load_user_objects = no in the instance's system config keeps its menu tree to the system copy (the per-user object directories are not searched, and a user config cannot turn them back on).

The commands an instance runs get ccfe's own PATH, not the one of the shell that started it: /usr/bin:/bin:/usr/local/bin:/sbin:/usr/sbin, then the path of ccfe.conf (default $HOME/bin), then the folder of the menu or form. The helper programs of an instance must therefore be in one of those directories, or their directory must be added with path in the instance's config file.

Running a menu tree as its own instance

A menu tree can live in another repository (a deployment repo with an operator-menu/ directory holding ops.menu, ops.d/*.form and so on) and be installed on a host as an instance. ccfe-build install-instance does the whole job in one command:

ccfe-build install-instance ops --from operator-menu            # for me: ~/.local/...
sudo ccfe-build install-instance ops --from operator-menu --system --restricted
ops                                                             # opens ops.menu

It

  • checks every menu and form in the tree with ccfe -k -W first, and installs nothing if one of them fails, or if the tree holds anything but plain files and directories (a .git directory is left out of the copy);
  • copies the tree into the instance's object directory, NAME under ~/.local/share/ccfe/ (--user, the default) or under the system objects directory (--system, which only root may run), replacing an earlier copy by renames;
  • writes NAME.conf beside the config (~/.config/ccfe/ or the system etc directory), with restricted = yes for --restricted;
  • links NAME to the ccfe program, in ~/.local/bin or /usr/local/bin (--bin DIR chooses another directory).

With --system the installed tree is owned by root with modes 0755 and 0644, and the result is checked with the rule restricted mode reads by (an object directory counts only when it, and every directory above it, is owned by root or by the owner of the installed program and is not writable by group or others); an install that fails the check is removed again. That is why --restricted needs --system: restricted mode reads no per-user directory. ccfe-build check-trusted [-r] PATH... runs the same check on any path.

Running the command again with a changed tree replaces the objects. ccfe-build uninstall-instance NAME [--system] takes away what the install made and nothing else: the paths are recorded in NAME.instance next to the config file, and the command refuses a record that names anything other than the instance's own paths. An object directory, config file or link that is already there and was not installed this way is never overwritten.

Restricted mode

For constrained deployments (a kiosk, an operator console, a restricted login), set restricted = yes in the global { } section of the config. CCFE then disables the F7 shell escape and the runnable-script save, and refuses the system: / exec: verbs unless the target program is on an allowlist:

global {
  restricted       = yes
  restricted_allow = top, df, uptime
}

Allowlist entries are bare program names or absolute paths. A bare name matches only a program found in a system directory of the restricted PATH (owned by root, not world-writable); an absolute entry matches that exact path. The typed command's first word is resolved against that PATH only, so ./top, /tmp/x/top or a symlink named top elsewhere do not satisfy an entry top; a word containing a slash is accepted only when it equals an absolute allowlist entry. The restricted PATH has no $HOME entry, no . and no relative entry.

run: items and command: pick-lists follow the same rule under restricted mode: they are executed as argv from the allowlist, never through a shell, and a command that uses shell syntax (;, |, &&, redirection, $(...)) or names a program that is not allowlisted is refused. Write restricted menus with simple commands, or call a wrapper script that is itself allowlisted. Outside restricted mode run(argv):PROG ARGS does the same for one item: no shell, so a field value with ; or $(...) stays one argument.

Do not allowlist a pager or editor (less, more, vi, man, ...) unless you know it cannot start a shell: such programs have a shell escape (less's ! command). Restricted children get LESSSECURE=1, which turns that escape off in less, and SHELL, EDITOR, VISUAL and PAGER are pinned to values that cannot start a shell (/bin/false, /bin/false, /bin/false, cat), but other programs have escapes of their own.

Saving output. In the output browser, a restricted user types a plain file name only (no /, not starting with .); the file goes to $XDG_DATA_HOME/ccfe/saved (default ~/.local/share/ccfe/saved, created mode 0700). "Save as script" is not offered. Everywhere, a save is created with O_EXCL|O_NOFOLLOW, mode 0600, and never follows a symlink; an existing name is replaced only after a confirmation, through a temporary file and a rename. An output filter that names an external program must pass restricted_allow; the built-in json filter always may.

Field values are always exported to action commands as CCFE_FIELD_<ID> environment variables (e.g. run:grep -- "$CCFE_FIELD_PATTERN" /var/log/syslog). %{PATTERN} in an action is also safe: it is replaced by the value as one quoted shell word (run:grep -- %{PATTERN} /var/log/syslog), so the value cannot add arguments or commands. %r{PATTERN} is the raw text, for the rare action that passes shell syntax on purpose, and ccfe -k warns about a placeholder written inside quotes ('%{PATTERN}'), which would be quoted twice.

Deploying it as a boundary. With restricted = yes, CCFE enforces the policy so a logged-in user cannot step around it:

  • system: / exec: are run shell-free (by argv) and never fall back to a shell: a command that cannot be parsed, is empty or cannot be executed is an error (status 127), not a shell command. The allowlist's match therefore holds — system: df; sh chaining and %{FIELD} metacharacters cannot reach a shell. run: actions and command: pick-lists follow the allowlist too (see above), and so do a form's init { } block and a field's default = command:: each is run as a program from the allowlist, and one that needs a shell (;, |, &&, $(...)) is refused.
  • Once restricted is on, only trusted configuration files and menu/form object directories are read. A path is trusted when it, and every directory above it, is owned by root (or by the account that owns the installed program) and is not writable by group or others. The per-user locations (~/.config/ccfe, ~/.local/share/ccfe, ~/.ccfe) are skipped outright: owners can chmod their own files, so making a file read-only does not make it trusted. A system config that sets restricted = yes therefore cannot be undone from the home directory, and a user cannot add their own menus. For a source install the system config is <prefix>/etc/ccfe.conf; the deb, rpm, apk and OpenWrt packages keep it at /etc/ccfe/ccfe.conf (a real config file that upgrades leave alone) and link it into the program tree.
  • -R stays on whatever a trusted config file says (restricted = no is overridden), and a configuration reload (Apply configuration now) applies the same rules, so it cannot bring a dropped directory back.
  • The directory overrides in the environment (CCFE_PREFIX, CCFE_ETC_DIR, CCFE_BIN_DIR, CCFE_LOG_DIR, CCFE_MSG_DIR, CCFE_OBJ_DIR, CCFE_THEME_DIR, CCFE_KEYMAP_DIR) are ignored under restricted mode and the installed locations are used. If the configuration (not -R) turns restricted mode on while such overrides are set, ccfe restarts itself as ccfe -R without them.
  • Message files are plain data (ID = "text"), never evaluated as Perl.
  • -d (debug log) is refused unless a trusted config file sets permit_debug = yes, and a refused -d writes no debug line to the log.

For this to hold, deploy CCFE with root-owned, group/world-unwritable config and object directories (and parents) and run it as a non-root user (the usual restricted-login setup). Defence in depth — OS-level confinement (a restricted shell, a container, seccomp/AppArmor) — is still worthwhile for a true kiosk; the next two sections say how the login itself is made a boundary. The analysis and history are in docs/history/M8-AUDIT.md and docs/history/TECH-DEBT.md.

ccfe -R (--restricted) forces restricted mode for a single run regardless of config — it can only tighten (there is no flag to turn it off) and it also makes the object search ignore untrusted directories. It's the primitive the login-shell setup uses.

Launching CCFE at login

Configure CCFE → Login / shell setup (the ccfe-login helper) makes CCFE start automatically on an interactive login or SSH session — for the current user, or system-wide via /etc/profile.d (which needs root). It hooks the shell profile and fires on interactive logins only, so scp, sftp, rsync and ssh host command are unaffected.

Tick restricted kiosk mode and the hook launches ccfe -R and replaces the login shell with it, so the session is sandboxed and quitting the menu logs out:

ccfe-login install-user   -r      # kiosk menu for me on next login
ccfe-login install-system -r      # kiosk for every user (needs root)
ccfe-login status                 # what is enabled
ccfe-login uninstall-user         # undo
ccfe-login --help                 # usage

The block it writes is marker-delimited and idempotent, so it is safe to re-apply or remove. A restricted CCFE session refuses these reconfiguration commands.

Bash reads only the first of ~/.bash_profile, ~/.bash_login and ~/.profile that exists. ccfe-login status and install-user warn when a hooked file sits behind one of those that has no hook, since a bash login never reaches it.

This is not a security boundary on its own. The hook lives in a file the user owns and runs only when a login shell reads it. A user can bypass it with ssh -t host bash (an interactive shell that is not a login shell reads no profile), by editing ~/.profile over sftp, by creating ~/.bash_profile (bash reads it before ~/.profile), or with commands in ~/.ssh/rc, which runs before the shell. Use ccfe-login for a convenience menu. For a kiosk, make the login itself run ccfe:

  • In sshd_config, give the kiosk account (or group) a Match block with ForceCommand /usr/bin/ccfe -R (or /usr/bin/NAME -R for an instance), AllowTcpForwarding no, X11Forwarding no and PermitTTY yes. ForceCommand applies to every shell, command and subsystem request and ignores ~/.ssh/rc; because it covers subsystems, sftp starts ccfe, not a file server, so do not enable internal-sftp for that account.
  • The command is run through the account's login shell with -c. Give the account a login shell that is not an interactive shell: a root-owned wrapper, listed in /etc/shells, that does exec /usr/bin/ccfe -R and ignores its arguments.
  • Install the config and menus root-owned and not writable by group or others (ccfe-build install-instance NAME --from DIR --system --restricted does that); restricted mode reads nothing from the account's home. Keep the home directory itself root-owned, or at least without writable startup files (~/.profile, ~/.bashrc), so the shell that runs the command finds none of the user's own to read.
  • Allowlist the programs the menus run in restricted_allow, and give the account no sudo rights beyond what those menus need.

Colour and styles

CCFE is monochrome by default. When the terminal supports colour (and NO_COLOR is unset and the layout is not Simple), it pre-creates the standard foreground colour pairs, so any *_attr setting in the config may reference COLOR_PAIR(n) — 1=red, 2=green, 3=yellow, 4=blue, 5=magenta, 6=cyan, 7=white — over the terminal's own background, combinable with A_BOLD etc.

This themes the run-output (browser_global), the form fields (field_attr / active_field_attr), and the menus themselves (menu_global):

menu_global {
  screen_attr   = COLOR_PAIR(6)                       # menu text
  selected_attr = COLOR_PAIR(3) | A_REVERSE | A_BOLD  # highlight bar
  title_attr    = COLOR_PAIR(3) | A_BOLD              # header
  key_attr      = COLOR_PAIR(2) | A_REVERSE | A_BOLD  # control keys
}
browser_global {
  stderr_attr = COLOR_PAIR(1) | A_BOLD                # errors in red
}

title_attr (the header) and key_attr (the function-key bar) apply to every screen — menus, forms and the output browser.

CCFE was modelled on AIX's SMIT, and ready-made themes ship under <prefix>/share/ccfe/themes/: ccfe.conf.smit (the classic monochrome original), ccfe.conf.smit-color (the colour version, including the header and control keys), ccfe.conf.smit-panel (a panelled white-on-blue look) and ccfe.conf.console (a Linux-console scheme). Drop one in as your ccfe.conf, or use it as an instance config: symlink the binary to smit and save it as smit.conf, so smit gets its own scheme and menu tree (see One binary, many menu trees above).

You don't have to copy a theme by hand, though — Configure CCFE → Select colour theme lists the installed themes and applies the chosen one at runtime (next section).

Configuring CCFE from the menu

Run ccfe config (or pick Configure CCFE from the default menu) for a guided wizard that writes your personal ~/.config/ccfe/ccfe.conf — screen layout, behaviour toggles, shell/restricted mode, config variables — without an editor. Several settings can then be changed while CCFE is running and take effect immediately, no restart:

  • Reload — Apply configuration now re-reads the config and re-applies colours, key bindings, behaviour toggles and variables. (Switching the mouse or the screen layout, and restricted mode, still need a restart; the confirmation says so.) Bind it to a key with keymap { reload = ... } for instant feedback while you tune a config.

  • Colour themes — Select colour theme lists the themes installed under share/ccfe/themes/, saves your choice and applies it at once — the same as a theme = NAME line in the config.

  • Key maps — Select key map switches between shipped presets: classic (the F1–F9 SMIT layout), safe (every function also on an Alt-letter, for terminals that grab the function keys), nano (Ctrl chords) and mc. Or spell your own out with a keymap { } section — each function can take an F-key, a Meta chord (M-l), a Ctrl chord (^G) or a plain key, primary plus alternates:

    keymap {
      list = F2, M-l
      back = F4, M-b, ^B
      exit = F9, M-q
    }
    

    (A lone Esc is still "cancel"; Meta chords are disambiguated by a short timeout — the ESCDELAY environment variable, default 25 ms.)

  • Search — Search menus and forms (also on the main menu, or bind the search function to a key) scans every menu and form on the path — titles, top/bottom text, item descriptions and form field labels — and jumps to the match you pick.

Notifications. Point notify_file at a file and CCFE shows its contents as a banner whenever a menu screen is entered (each distinct write once) — a simple way for a producer to reach a logged-in operator.

Generating menus

A menu does not have to be written by hand when a program can describe what it offers. ccfe-gen turns a machine-readable description into menus and forms, so that a new item appears in the menu with no file to edit. The first generator is ctrl-exec, which builds an operator menu for the ctrl-exec command dispatcher from its discovery output (the example here; CCFE does not depend on it):

ctrl-exec-cli discovery --json > discovery.json
ccfe-gen ctrl-exec --from discovery.json --out ~/.local/share/ccfe/ccfe
ccfe ctrl-exec

Without --from the tool runs ctrl-exec-cli discovery --json itself; --cli names the program.

What it writes into the objects directory given with --out:

  • ctrl-exec.menu – Fleet status, Run status (asks for a request id) and one item per script name, the union over every host that reports it.
  • ctrl-exec.d/NAME.form – one form per script. The first field, Hosts, is a multi-value pick-list of the hosts that report the script. The other fields come from the script's schema: a boolean gives its flag, a string or integer is a text field, an enum is a pick-list, an array is a field where each blank-separated word is one argument; default, required and the first sentence of the description are used too. A script with no schema gets one free Arguments field.

Each action is run(argv):ctrl-exec-cli run --json %{HOSTS} --script NAME -- ... with the built-in json output filter. It runs without a shell and every field value is one quoted word, so a value cannot add an argument or a command. confirm is added unless the schema says the script is read-only. A script that needs confidential input gets no form.

The tree is built beside the old one, every file is checked with ccfe -k and only then renamed into place; if anything fails nothing changes. Only ctrl-exec.menu and ctrl-exec.d in the objects directory are replaced, so menus written by hand next to them are never touched. Run the command again when the scripts of the fleet change. ccfe runs ctrl-exec-cli by name from its own path plus the path of ccfe.conf; add the program's directory there (or give --cli an absolute path), and under -R list it in restricted_allow. See ccfe-gen(1).

Calling an external CLI

A form that fronts a command-line program with hosts and arguments (the worked example is ctrl-exec-cli; ccfe does not depend on it) needs these rules. The full text is the CALLING AN EXTERNAL CLI section of man ccfe_form.

  • %{ID} is the field value as one quoted shell word; %r{ID} is raw text; $CCFE_FIELD_<ID> holds the value as typed. Never put %{ID} inside quotes.
  • run(argv):PROG ARGS runs without a shell, so a value cannot add an argument or a command. run: uses the shell; system: hands the terminal to the command.
  • The command of a run: action has no terminal: its standard input is empty (end of file at once), so a prompting program sees no answer. Pass a non-interactive flag (--yes or similar) as an argument, or use system:. Pick-list, default and init commands also read an empty standard input.
  • Commands get ccfe's own PATH (/usr/bin:/bin:/usr/local/bin:/sbin:/usr/sbin, the path of ccfe.conf, the form's folder): add the program's directory to path, or write its absolute name.
  • The browser shows Ok, FAILED [ES=n] or SIGNAL N (status 128+N). filter = json (menu item) or filter { json } (form) shows JSON output as a table; json(value=KEY,descr=KEY,path=a.b):COMMAND fills a pick-list from JSON, and the command can use other fields, so a list can depend on one above it. An empty or failing list leaves the field's value unchanged. A list command that exits non-zero is an error (shown with its exit status, its output discarded): write grep ... || true where an empty result is normal.
  • run(confirm): asks Yes/No (default No). log writes a form action's command to the log, with hidden values as ****.
  • Under -R the program must be in restricted_allow and the action must be a plain run(argv) command; the files must sit in a root-owned directory.
  • ccfe-gen writes such forms from the CLI's own description, and ccfe -k -W checks them; ccfe-build install-instance installs the tree as an instance (see above). Persistent field values (persist = YES) are saved under $XDG_DATA_HOME/ccfe/persistent, mode 0600, never through a symlink.

Docker

A minimal image (stock Debian + libcurses-perl) is provided:

docker build -t ccfe .
docker run -it ccfe          # opens the demo menu
docker run -it ccfe sysmon   # jump straight to a menu/form

"Remembered commands" shell (compose)

docker-compose.yml turns CCFE into a throwaway shell of the admin/debug commands you always forget — disk/memory/CPU, processes, networking, plus parameterised find and tail forms and a Docker submenu:

docker compose run --rm ccfe          # open the remembered-commands menu
docker compose run --rm ccfe demo     # or jump to the bundled demo / sysmon

The curated objects live in docker/objects/ and are bind-mounted read-only, so you can edit the menu on the host without rebuilding (ccfe -l points at them). The Docker items (docker ps / images / logs) need the host daemon — un-comment the docker.sock mount in the compose file to enable them.

Plugins

CCFE is extended purely by dropping declarative files onto its search path, which for a binary invoked as ccfe is, in order:

  1. <prefix>/share/ccfe/objects/ccfe/ — system-wide
  2. ~/.local/share/ccfe/ccfe/ — your XDG data dir
  3. ~/.ccfe/ccfe/ — legacy fallback

The building blocks:

File Purpose
<name>.menu a static menu — a list of items, each with an action
<name>.menu/ directory a dynamic menu — a definition file plus any number of *.item files merged into it
<name>.form a form — labelled fields the user fills in
<name>.d/ directory a collection of related .form files
*.item a single menu item a plugin drops into another menu's .menu/ directory to graft itself on

Items dispatch through action verbs — menu: (open another menu), form: (open a form), run: (run a command in a pager-like view), system: (hand the terminal to a full-screen command), and exec: (replace CCFE with the command) — with modifiers like (confirm,log,wait_key). Form fields can be populated from a list_cmd (command: runs a program, const: is a fixed list, json(value=KEY,descr=KEY,path=a.b): runs a program that prints JSON) so dropdowns can be driven by live data.

Worked example — the sysmon plugin

src/ccfe-plugin-sysmon/ is a complete, installable example. It ships:

  • sysmon.menu — its top-level menu (ps, top, a sar form, …),
  • sysmon.d/*.form — the forms those items open,
  • sysmon.item — the one item that grafts sysmon onto the demo menu,
  • its own install.sh.

A tree of your own that should not graft onto another menu but run as a front-end of its own is installed as an instance instead (see Running a menu tree as its own instance above).

Install it and run it:

cd src/ccfe-plugin-sysmon
sh install.sh
ccfe sysmon          # or pick "System resources..." from `ccfe demo`

Writing your own

The quickest start is a per-user menu. Create ~/.local/share/ccfe/ccfe/mytools.menu:

title {
  My tools
}
item {
  id     = DISK
  descr  = Disk usage
  action = run:df -h
}
item {
  id     = LOGS
  descr  = Tail the syslog
  action = run:tail -n 200 /var/log/syslog
}
item {
  id     = NET
  descr  = Network connections
  action = run:ss -tunap
}

Each attribute goes on its own line: a one-line item { id=DISK descr=... } reads the whole line as the id.

Then run ccfe mytools. See the manual pages under src/man/ (or man ccfe, man ccfe_menu, man ccfe_form after install) for the full file-format reference, and ccfe-plugin-sysmon for forms and list_cmd dropdowns.

Build menus and forms without an editor

CCFE can build CCFE. Run ccfe builder for a guided menu that creates and extends menus, forms and config — writing them to your XDG directory and validating each with ccfe -k. It is plain CCFE content driving the ccfe-build helper (the same helper is usable directly: ccfe-build new-menu mytools "My tools", ccfe-build add-item mytools DISK "Disk usage" "run:df -h"). Field values reach the helper through $CCFE_FIELD_*, so nothing you type is shell-interpolated.

The builder also manages ownership of your objects — Change object ownership (ccfe-build chown <name> <user>[:<group>]) lists the local users and groups to pick from and changes the owner of one of your own menus/forms. Changing to another user usually needs root; if you lack the privilege the error is shown plainly.

Validate a menu or form without opening the UI (useful for authors and CI):

ccfe -k mytools          # OK: menu "mytools" -- title "My tools", 3 item(s)
ccfe -k sysmon.d/sar     # checks a form; exits non-zero on a mistake
ccfe -k -W mytools       # -W (--strict): a warning fails the check too

-k exits 0 when the object is sound, 1 when it is not and 2 when the name is not on the search path. It reports each mistake as an ERROR line, with the file and line where it is known, and exits 1 on any of: a parse error; a menu or form with an empty title; an item with no description; an item or form with no action; an unknown verb or an option the verb does not take (rnu:, run(cnofirm):; argv is for run, wait_key for system); an action verb that needs an argument and has none; a list field whose list_cmd source is none of command:, const: or json(...):. The checks for a placeholder written inside quotes ('%{ID}') and for a field option ending in a quote are WARNING lines: they exit 0, and exit 1 under -W. Use -W in CI and when installing a menu tree for others.

Backing up & version-controlling your setup

Your CCFE setup is just text files: the config (ccfe.conf, plus any instance <name>.conf) and your menus/forms. The system copies sit under your install prefix; your personal additions follow the XDG base directories:

<prefix>/etc/ccfe.conf               system config
<prefix>/share/ccfe/objects/<name>/  system menus & forms
~/.config/ccfe/<name>.conf           your per-user config
~/.local/share/ccfe/<name>/          your per-user menus & forms

Because they're plain text, put them under version control — keep a small git repo of just your config and menus so changes are reviewable and reversible:

mkdir -p ~/ccfe-config && cd ~/ccfe-config
git init
cp -r ~/.config/ccfe ./config        # your per-user config, if any
cp -r ~/.local/share/ccfe ./menus    # your per-user menus, if any
git add . && git commit -m "My CCFE config and menus"

To restore on a new machine, install CCFE (or the package), then copy these files back into place. ccfe -k <name> parse-checks a menu/form after you restore or edit it.

Tests

make check          # the gate: tests + perlcritic + perltidy + shellcheck
make test           # the tests only

make check is what CI runs. It sets CCFE_REQUIRE_PTY=1, so a test that would skip fails instead: no pseudo-terminal, no Curses, no installer, a failed install, a missing tool or running as root all count as failures, and a green gate never means "skipped quietly". The one skip that stays a skip is a run-time optional module (Text::CharWidth), whose TAP line names it. Without that variable (plain make test, or prove -lr t/ from src/) such tests skip, which suits a build sandbox with no /dev/pts. The packages the whole gate needs are listed in docker/ci/packages.txt.

A perlcritic, PPI or perltidy of another version than your own can disagree with your local gate. CI runs the gate in a container image, and you can run the same image locally, without pushing:

docker build -t ccfe-ci docker/ci    # once, and to pick up a newer Ubuntu
./scripts/ci-local                   # make check in that image, no network

ci-local checks the working tree as it is (committed or not) in a temporary copy, as an unprivileged user with your uid. To compare another toolchain, build the image from another base and point ci-local at it:

docker build --build-arg BASE=ubuntu:24.04 -t ccfe-ci:24.04 docker/ci
CCFE_CI_IMAGE=ccfe-ci:24.04 ./scripts/ci-local

The suite (400+ tests) covers: that the program compiles; that the .menu / .form / .item plugin file formats still parse against the demo and sysmon fixtures; a source-level regression guard for the historical form crash; the pure CCFE::* parser/layout/theme modules; and end-to-end terminal tests that batch-install CCFE into a temp prefix and drive it on a real pseudo-terminal — including the runtime reload, theme/key-map switching (and Meta-key bindings), menu search, notifications, ownership and login-shell setup. The parser tests load the program headlessly (via the CCFE_TESTING guard) so no terminal is required; the tty tests bring their own pty (pure core Perl, see t/lib/CCFE/Test/Pty.pm) and skip themselves where no Linux pty / Curses is available (unless CCFE_REQUIRE_PTY=1, above). Every test that runs ccfe loads CCFE::Test::Env (the pty helper does it for the pty tests), which gives it a private HOME and XDG tree, so the developer's own configuration is never read. The pty helper always gives the program a fixed TERM=xterm, whatever the developer's terminal is. A pty test ends by checking the program's log (CCFE::Test::LogGate): any Perl warning that ccfe wrote there while curses owned the screen fails the test; the list of excused warnings is empty.

About

Clone of Curses Command Front End (CCFE) from ccfe.altervista.org.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages