feat: add category display options

This commit is contained in:
2026-03-16 18:13:28 +02:00
parent 90e4173ffc
commit e464ea24ac
6 changed files with 123 additions and 66 deletions

View File

@@ -150,10 +150,11 @@ For a full breakdown with all the supported options, see
### Global Options
| Field | Type | Description |
| --------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ------------------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `debug` | Boolean | Enable or disable debug mode. Default: `false`. |
| `check_updates` | Boolean | Enable or disable checking for updates before running operations. Default: `false`. |
| `summary` | Boolean | Enable or disable the installation summary at the end. Default: `true`. |
| `category_display` | String | Controls how category headers are rendered. Values: `border` (default), `border-compact`, `minimal`. |
| `defaults` | Object | Defaults to apply to all installer types, such as specifying supported platforms or commonly used flags. |
| `env` | Object | Environment variables that will be set for the context of the installer. OS env vars are passed, and may be overridden for this config and all of its installers here. |
| `install` | Array | Installation steps to execute. |

View File

@@ -12,6 +12,18 @@ import (
"gopkg.in/yaml.v3"
)
// CategoryDisplayMode controls how category headers are rendered.
type CategoryDisplayMode string
const (
// CategoryDisplayBorder renders categories with a full border and spacing.
CategoryDisplayBorder CategoryDisplayMode = "border"
// CategoryDisplayBorderCompact renders categories with a border but no spacing.
CategoryDisplayBorderCompact CategoryDisplayMode = "border-compact"
// CategoryDisplayMinimal renders categories without a border or spacing.
CategoryDisplayMinimal CategoryDisplayMode = "minimal"
)
// AppConfig represents the main application configuration.
type AppConfig struct {
// Debug enables or disables debug mode.
@@ -20,6 +32,8 @@ type AppConfig struct {
CheckUpdates *bool `json:"check_updates" yaml:"check_updates"`
// Summary enables or disables the installation summary at the end.
Summary *bool `json:"summary" yaml:"summary"`
// CategoryDisplay controls how category headers are rendered.
CategoryDisplay *CategoryDisplayMode `json:"category_display" yaml:"category_display"`
// Install is a list of installers to run.
Install []InstallerData `json:"install" yaml:"install"`
// Defaults provides default configurations for installer types.
@@ -60,6 +74,14 @@ type AppConfigDefaults struct {
Type *map[InstallerType]InstallerData `json:"type" yaml:"type"`
}
// GetCategoryDisplay returns the effective category display mode, defaulting to "border".
func (c *AppConfig) GetCategoryDisplay() CategoryDisplayMode {
if c.CategoryDisplay != nil {
return *c.CategoryDisplay
}
return CategoryDisplayBorder
}
// Environ returns the combined environment variables as a slice of strings.
func (c *AppConfig) Environ() []string {
return utils.EnvMapAsSlice(utils.CombineEnvMaps(c.Env, c.PlatformEnv.Resolve()))

View File

@@ -5,37 +5,39 @@ Here is a breakdown of all configuration options:
## Global Options
- **`install`** (Array)
- Installation steps to execute.
- See [Installer Configuration](./installer-configuration.md) for supported types and options that
you can provide.
- **`debug`** (Boolean)
- Enable or disable debug mode.
- Default: `false`.
- **`check_updates`** (Boolean)
- Enable or disable checking for updates before running operations.
- Default: `false`.
- **`summary`** (Boolean)
- Enable or disable the installation summary at the end.
- The summary shows newly installed and upgraded software in a hierarchical format.
- Default: `true`.
- **`defaults`** (Object)
- **`category_display`** (String)
- Controls how category headers are rendered in the output.
- Values:
- `border` — Full border with spacing before and after (default).
- `border-compact` — Border without spacing before and after.
- `minimal` — Plain text without border or spacing.
- Default: `border`.
- **`defaults`** (Object)
- Defaults to apply to all installer types, such as specifying supported platforms or commonly
used flags.
- **`defaults.type`**
A mapping between each type (key) and their default options (value).
- See [Installer Configuration](./installer-configuration.md) for supported types and options
that you can override.
@@ -47,8 +49,8 @@ Here is a breakdown of all configuration options:
- **`machine_aliases`** (Object)
- A mapping of friendly names to machine IDs.
- Use `sofmani --machine-id` to get the machine ID for each of your machines.
- These aliases can then be used in installer `machines.only` and `machines.except` fields
instead of the raw machine IDs.
- These aliases can then be used in installer `machines.only` and `machines.except` fields instead
of the raw machine IDs.
- Example:
```yaml
machine_aliases:
@@ -63,6 +65,7 @@ Here is a breakdown of all configuration options:
debug: false
check_updates: true
summary: true
category_display: border
defaults:
type:
brew:

View File

@@ -11,16 +11,14 @@ entries that display a bordered header in the output but don't perform any insta
### Fields
- **`category`**
- **Type**: String (required for category entries)
- **Description**: The category name to display. When this field is present, the entry is treated
as a category header, not an installer.
- **`desc`**
- **Type**: String (optional)
- **Description**: An optional description shown below the category name. Supports multi-line
text with automatic word wrapping. Existing line breaks are preserved.
- **Description**: An optional description shown below the category name. Supports multi-line text
with automatic word wrapping. Existing line breaks are preserved.
### Example
@@ -54,7 +52,11 @@ install:
### Output
Categories are displayed with a bordered header:
The appearance of category headers is controlled by the top-level `category_display` option.
#### `border` (default)
Categories are displayed with a bordered header and spacing before/after:
```
┌──────────────────────────────────────────────────────────┐
@@ -72,7 +74,27 @@ With a description:
└──────────────────────────────────────────────────────────┘
```
The box width adapts to narrower terminals (minimum of terminal width or 60 characters).
#### `border-compact`
Same as `border`, but without the empty lines before and after the box.
#### `minimal`
Categories are displayed as plain text without any border or spacing:
```
Development Tools
```
With a description:
```
System Utilities
Tools for system maintenance and monitoring.
```
The box width (for `border` and `border-compact`) adapts to narrower terminals (minimum of terminal
width or 60 characters).
---
@@ -82,33 +104,28 @@ These fields are shared by all installer types. Some fields may vary in behavior
`type`.
- **`name`**
- **Type**: String (required)
- **Description**: Identifier for the step. It does not have to be unique, but is usually used to
check for the app's existence (can be overridden using `bin_name`).
- **`type`**
- **Type**: String (required)
- **Description**: Type of the step. See [supported types](#supported-type-of-installers) for a
comprehensive list of supported values.
- **`enabled`**
- **Type**: String or Boolean (optional)
- **Description**: Enable or disable the step. Disabled steps are not run. This can either be a
static boolean (`true` or `false`), or a command that returns a success status code for true, or
a failure for false.
- **`tags`**
- **Type** String (optional)
- **Description**: Arbitrary tags to attach to an installer. These can later be used to filter
this installer in or out when running sofmani. This should be a string containing
space-separated tags.
- **`platforms`**
- **Type**: Object (optional)
- **Description**: Platform-specific execution controls. See `platforms` subfields below.
- **Subfields**:
@@ -121,12 +138,11 @@ These fields are shared by all installer types. Some fields may vary in behavior
- **Description**: Platforms where the step should **not** execute; replaces `platforms.only`.
- **`machines`**
- **Type**: Object (optional)
- **Description**: Machine-specific execution controls. Use this to run installers only on
specific machines. Get the machine ID by running `sofmani --machine-id`. You can use either
raw machine IDs or aliases defined in the top-level `machine_aliases` configuration.
See `machines` subfields below.
specific machines. Get the machine ID by running `sofmani --machine-id`. You can use either raw
machine IDs or aliases defined in the top-level `machine_aliases` configuration. See `machines`
subfields below.
- **Subfields**:
- **`machines.only`**
- **Type**: Array of Strings
@@ -137,26 +153,22 @@ These fields are shared by all installer types. Some fields may vary in behavior
- **Description**: Machine IDs or aliases where the step should **not** execute.
- **`steps`**
- **Type**: Array of Installers
- **Description**: Sub-steps for `group` type. Allows nesting multiple steps together. Ignored for
all other types.
- **`opts`**
- **Type**: Object (optional)
- **Description**: Step-specific options and configurations. Content varies depending on the
`type`. See [supported types](#supported-type-of-installers) for a comprehensive list of
supported values.
- **`bin_name`**
- **Type**: String (optional)
- **Description**: Binary name for the installed software, used instead of `name` when checking
for app's existence.
- **`check_has_update`**
- **Type**: String (shell script)
- **Description**: Shell command to check whether an update is available for the installed
software. This will override the default check provided by the corresponding `type`. The check
@@ -164,29 +176,24 @@ These fields are shared by all installer types. Some fields may vary in behavior
the app is up to date.
- **`check_installed`**
- **Type**: String (shell script)
- **Description**: Shell command to check if the step has already been installed. If the check
succeeds (exits with status 0), it means the app is already installed and can be skipped if not
checking for updates.
- **`pre_install`**
- **Type**: String (shell script)
- **Description**: Shell script to execute _before_ the step is installed.
- **`post_install`**
- **Type**: String (shell script)
- **Description**: Shell script to execute _after_ the step is installed.
- **`pre_update`**
- **Type**: String (shell script)
- **Description**: Shell script to execute _before_ the step is updated (if applicable).
- **`post_update`**
- **Type**: String (shell script)
- **Description**: Shell script to execute _after_ the step is updated (if applicable).
@@ -205,7 +212,6 @@ These fields are shared by all installer types. Some fields may vary in behavior
shell will be used.
- **`skip_summary`**
- **Type**: Boolean or Object (optional)
- **Description**: Exclude this installer from the installation summary. Useful for installers
that always run (like config sync scripts) and would clutter the summary output.
@@ -216,6 +222,7 @@ These fields are shared by all installer types. Some fields may vary in behavior
- **`skip_summary.install`**: Boolean - exclude from the "Installed" section of the summary.
- **`skip_summary.update`**: Boolean - exclude from the "Upgraded" section of the summary.
- **Examples**:
```yaml
# Skip from both install and update summaries
- name: sync-dotfiles
@@ -243,21 +250,18 @@ These fields are shared by all installer types. Some fields may vary in behavior
## Supported `type` of Installers
- **`shell`**
- **Description**: Executes arbitrary shell commands.
- **Options**:
- `opts.command`: The command to execute for installing.
- `opts.update_command`: The command to execute for updating.
- **`group`**
- **Description**: Executes a logical group of steps in sequence.
- Allows nesting multiple steps together.
- **Options**:
- `steps`: List of nested steps.
- **`git`**
- **Description**: Clones a git repository to a local directory.
- If `name` is a full git URL (https or SSH), the repository is cloned directly.
- If it is a repository path, e.g. `chenasraf/sofmani`, GitHub is assumed.
@@ -269,10 +273,8 @@ These fields are shared by all installer types. Some fields may vary in behavior
- `opts.update_flags`: Additional flags to pass only to `git pull`.
- **`github-release`**
- **Description**: Downloads a GitHub release asset. Optionally untar/unzip the downloaded file.
- **Options**:
- `opts.repository`: The repository to download from. Should be in the format:
`user/repository-name`
- `opts.destination`: The target directory to extract the files to.
@@ -285,7 +287,6 @@ These fields are shared by all installer types. Some fields may vary in behavior
This should either be a string, or a map of platforms to filenames.
You can use Go template syntax to insert dynamic values into the filename:
- `{{ .Tag }}` - the full tag name, e.g. `v1.0.0`
- `{{ .Version }}` - the version without the leading "v", e.g. `1.0.0`
- `{{ .Arch }}` - the system architecture in Go format, e.g. `amd64`, `arm64`
@@ -293,7 +294,9 @@ These fields are shared by all installer types. Some fields may vary in behavior
- `{{ .ArchGnu }}` - the architecture in GNU/Linux format, e.g. `x86_64`, `aarch64`
- `{{ .OS }}` - the current operating system, e.g. `macos`, `linux`, `windows`
**Legacy syntax (deprecated):** The old `{tag}`, `{version}`, `{arch}`, `{arch_alias}`, `{arch_gnu}`, and `{os}` tokens are still supported but deprecated. A deprecation warning will be logged at DEBUG level when they are used.
**Legacy syntax (deprecated):** The old `{tag}`, `{version}`, `{arch}`, `{arch_alias}`,
`{arch_gnu}`, and `{os}` tokens are still supported but deprecated. A deprecation warning will
be logged at DEBUG level when they are used.
Examples:
@@ -332,7 +335,6 @@ These fields are shared by all installer types. Some fields may vary in behavior
```
- **`manifest`**
- **Description**: Installs an entire manifest from a local or remote file.
- Every entry in the `install` array will be run, similar to how `steps` are run for `group`
installers.
@@ -350,7 +352,6 @@ These fields are shared by all installer types. Some fields may vary in behavior
`master`. Ignored for local files and raw HTTP URLs.
- **`rsync`**
- **Description**: Copy files from `source` to `destination` using rsync.
- **Options**:
- `opts.source`: Source directory/file.
@@ -358,7 +359,6 @@ These fields are shared by all installer types. Some fields may vary in behavior
- `opts.flags`: Additional rsync flags (e.g., `--delete`, `--exclude`).
- **`brew`**
- **Description**: Installs packages using Homebrew.
- **Options**:
@@ -369,7 +369,6 @@ These fields are shared by all installer types. Some fields may vary in behavior
- `opts.update_flags`: Additional flags to pass only to `brew upgrade`.
- **`npm`/`pnpm`/`yarn`**
- **Description**: Installs packages using npm/pnpm/yarn.
- Use `type: npm` for `npm install`, `type: pnpm` for `pnpm install`, and `type: yarn` for
`yarn install`.
@@ -379,7 +378,6 @@ These fields are shared by all installer types. Some fields may vary in behavior
- `opts.update_flags`: Additional flags to pass only during update.
- **`apt`/`apk`**
- **Description**: Installs packages using apt install or apt add.
- Use `type: apt` for `apt install`, and `type: apk` for `apk add`.
- **Options**:
@@ -388,7 +386,6 @@ These fields are shared by all installer types. Some fields may vary in behavior
- `opts.update_flags`: Additional flags to pass only during update.
- **`pacman`/`yay`**
- **Description**: Installs packages using pacman or yay (Arch Linux).
- Use `type: pacman` for official Arch repository packages.
- Use `type: yay` for AUR (Arch User Repository) packages.
@@ -400,7 +397,6 @@ These fields are shared by all installer types. Some fields may vary in behavior
- `opts.update_flags`: Additional flags to pass only during update.
- **`pipx`**
- **Description**: Installs packages using pipx.
- **Options**:
- `opts.flags`: Additional flags to pass to commands (fallback for install/update).
@@ -408,10 +404,8 @@ These fields are shared by all installer types. Some fields may vary in behavior
- `opts.update_flags`: Additional flags to pass only to `pipx upgrade`.
- **`docker`**
- **Description**: Pulls and runs Docker containers using `docker run`. Also supports update
checks by comparing image digests.
- The image is pulled from the registry (e.g., Docker Hub or GHCR) and started with the provided
options.
- If the container already exists, it will be started instead of run again.
@@ -419,17 +413,14 @@ These fields are shared by all installer types. Some fields may vary in behavior
- The container is always run with `--restart always -d`, unless overridden in a custom shell.
- **Required**:
- `name`: The full Docker image name, including tag (e.g.,
`ghcr.io/open-webui/open-webui:main`).
- `bin_name`: The container name to assign to the running instance (used in install and update
checks).
- **Options**:
- `opts.flags`: A string of flags to pass to `docker run` (e.g., ports, volumes, extra args).
These are appended after the default flags and before the image name.
- Example:
```yaml
@@ -443,7 +434,6 @@ These fields are shared by all installer types. Some fields may vary in behavior
This is useful if you're running on a platform like `darwin/arm64`, but want to compare
digests for a different image target (e.g., `linux/amd64`).
- Example:
```yaml

View File

@@ -248,9 +248,35 @@ func getBoxWidth() int {
return boxDefaultWidth
}
// CategoryDisplayMode controls how category headers are rendered.
type CategoryDisplayMode string
const (
// CategoryDisplayBorder renders categories with a full border and spacing.
CategoryDisplayBorder CategoryDisplayMode = "border"
// CategoryDisplayBorderCompact renders categories with a border but no spacing.
CategoryDisplayBorderCompact CategoryDisplayMode = "border-compact"
// CategoryDisplayMinimal renders categories without a border or spacing.
CategoryDisplayMinimal CategoryDisplayMode = "minimal"
)
// Category logs a category header with a decorative border.
// If desc is provided, it will be displayed below the category name with auto-wrapping.
func Category(name string, desc *string) {
// The displayMode controls the visual style: "border" (default), "border-compact", or "minimal".
func Category(name string, desc *string, displayMode CategoryDisplayMode) {
switch displayMode {
case CategoryDisplayMinimal:
categoryMinimal(name, desc)
case CategoryDisplayBorderCompact:
categoryBorder(name, desc, false)
default:
categoryBorder(name, desc, true)
}
}
// categoryBorder renders a category with box-drawing borders.
// If spaced is true, empty lines are added before and after.
func categoryBorder(name string, desc *string, spaced bool) {
boxWidth := getBoxWidth()
innerWidth := boxWidth - 4 // Account for "│ " and " │"
@@ -261,7 +287,9 @@ func Category(name string, desc *string) {
separator := boxLeftT + horizontalLine + boxRightT
// Log the header
if spaced {
Info("")
}
Info("%s", topBorder)
Info("%s", formatBoxLine(name, innerWidth))
@@ -274,8 +302,21 @@ func Category(name string, desc *string) {
}
Info("%s", bottomBorder)
if spaced {
Info("")
}
}
// categoryMinimal renders a category as plain text without borders or spacing.
func categoryMinimal(name string, desc *string) {
Info("%s", name)
if desc != nil && len(*desc) > 0 {
boxWidth := getBoxWidth()
for _, line := range wrapText(*desc, boxWidth) {
Info("%s", line)
}
}
}
// formatBoxLine formats a line of text to fit within the box.
func formatBoxLine(text string, innerWidth int) string {

View File

@@ -142,7 +142,7 @@ func runMain(cliConfig *appconfig.AppCliConfig) {
// Handle category entries - just log the header
if item.isCategory {
logger.Category(*item.data.Category, item.data.Desc)
logger.Category(*item.data.Category, item.data.Desc, logger.CategoryDisplayMode(cfg.GetCategoryDisplay()))
continue
}