refactor(cli): add validation logic + docs

This PR contains the following changes
- Add validation logic and dependencies
- Remove deprecated frame rate
- Bump string_pipeline to 0.12.0 (solves debug syntax from templates)
- Document cli functionality (markdown and docstrings)

---------

Co-authored-by: alexandre pasmantier <alex.pasmant@gmail.com>
This commit is contained in:
LM 2025-06-22 12:52:26 +02:00 committed by Alex Pasmantier
parent 601580953a
commit a2ebbb3557
8 changed files with 877 additions and 90 deletions

8
Cargo.lock generated
View File

@ -251,10 +251,8 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
@ -1851,18 +1849,16 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "string_pipeline"
version = "0.11.1"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0af3613597e31606b54dd5d62be86b8f50922b40d2b7d3d145146caf5c154c05"
checksum = "8d7043de9eb4072c03851ec3682a133c26b91b9f8fcc4d52bf911abe2614de12"
dependencies = [
"chrono",
"clap",
"clap_mangen",
"once_cell",
"pest",
"pest_derive",
"regex",
"serde_json",
"strip-ansi-escapes",
]

View File

@ -54,7 +54,7 @@ lazy-regex = { version = "3.4.1", features = [
], default-features = false }
ansi-to-tui = "7.0.0"
walkdir = "2.5.0"
string_pipeline = "0.11.1"
string_pipeline = "0.12.0"
ureq = "3.0.11"
serde_json = "1.0.140"
colored = "3.0.0"

522
docs/cli.md Normal file
View File

@ -0,0 +1,522 @@
# Television CLI Reference
Television (`tv`) is a cross-platform, fast and extensible general purpose fuzzy finder TUI. This document provides a comprehensive reference for all CLI options, modes, restrictions, and usage patterns.
## Table of Contents
- [Overview](#overview)
- [Operating Modes](#operating-modes)
- [Basic Usage](#basic-usage)
- [Arguments](#arguments)
- [Options](#options)
- [Subcommands](#subcommands)
- [Flag Dependencies and Restrictions](#flag-dependencies-and-restrictions)
- [Configuration](#configuration)
- [Template System](#template-system)
- [Examples](#examples)
- [Advanced Usage](#advanced-usage)
## Overview
Television supports two primary operating modes that determine how CLI flags are interpreted and validated:
1. **Channel Mode**: When a channel is specified, the application uses the channel's configuration as a base and CLI flags act as overrides
2. **Ad-hoc Mode**: When no channel is specified, the application creates a custom channel from CLI flags with stricter validation
## Operating Modes
### Channel Mode
**Activated when**: A channel name is provided as the first argument or via `--autocomplete-prompt`
**Behavior**:
- Channel provides base configuration (source commands, preview commands, UI settings)
- CLI flags act as **overrides** to channel defaults
- More permissive validation - allows most combination of flags
- Minimal dependency checking since channel provides sensible defaults
**Example**:
```bash
tv files --preview-command "bat -n --color=always {}"
```
### Ad-hoc Mode
**Activated when**: No channel is specified and no `--autocomplete-prompt` is used
**Behavior**:
- Creates a custom channel on-the-fly from CLI flags
- Requires `--source-command` to generate any entries
- **Stricter validation** ensures necessary components are present
- All functionality depends on explicitly provided flags
**Example**:
```bash
tv --source-command "find . -name '*.rs'" --preview-command "bat -n --color=always {}"
```
## Basic Usage
```
tv [OPTIONS] [CHANNEL] [PATH]
```
### Arguments
Television has intelligent positional argument handling with special path detection logic.
#### Position 1: `[CHANNEL]`
**Purpose**: Channel name to activate Channel Mode
- **Standard behavior**: When a valid channel name is provided, activates Channel Mode
- **Special path detection**: If the argument exists as a path on the filesystem, it's automatically treated as a working directory instead
- **Effect when path detected**: Switches to Ad-hoc Mode and uses the path as the working directory
- **Required**: No (falls back to `default_channel` from the global config)
- **Examples**:
```bash
tv files # Uses 'files' channel
tv /home/user/docs # Auto-detects path, uses as working directory
tv ./projects # Auto-detects relative path
```
#### Position 2: `[PATH]`
**Purpose**: Working directory to start in
- **Behavior**: Sets the working directory for the application
- **Required**: No
- **Precedence**: Only used if Position 1 was not detected as a path
- **Default**: Current directory
- **Example**: `tv files /home/user/projects`
#### ⚡ Smart Path Detection Logic
Television automatically detects when the first argument is a filesystem path:
1. **Path Check**: If Position 1 exists as a file or directory on the filesystem
2. **Mode Switch**: Automatically switches to Ad-hoc Mode (no channel)
3. **Directory Assignment**: Uses the detected path as the working directory
4. **Requirement**: When this happens, `--source-command` becomes required (Ad-hoc Mode rules apply)
**Examples of Smart Detection**:
```bash
# No arguments - uses default_channel from config
tv
# Channel name provided - Channel Mode
tv files
# Existing path provided - triggers path detection → uses default_channel
tv /home/user/docs # Uses default_channel in /home/user/docs directory
# Non-existent path - treated as channel name → error if channel doesn't exist
tv /nonexistent/path # Error: Channel not found
# Channel + explicit working directory - Channel Mode
tv files /home/user/docs
# The key nuance: same name, different behavior based on filesystem
tv myproject # Channel Mode (if 'myproject' is a channel name)
tv ./myproject # Channel Mode with default_channel (if './myproject' directory exists)
# Ambiguous case - path detection takes precedence
tv docs # If 'docs' directory exists → default_channel + path detection
# If 'docs' directory doesn't exist → 'docs' channel
```
> **💡 Tip**: This smart detection makes Television intuitive - you can just specify a directory and it automatically knows you want to work in that location.
## Options
Television's options are organized by functionality. Each option behaves differently depending on whether you're using Channel Mode (with a channel specified) or Ad-hoc Mode (no channel).
### 🎯 Source and Data Options
#### `--source-command <STRING>`
**Purpose**: Defines the command that generates entries for the picker
- **Channel Mode**: Overrides the channel's default source command
- **Ad-hoc Mode**: ⚠️ **Required** - without this, no entries will be generated
- **Example**: `--source-command "find . -name '*.py'"`
#### `--source-display <STRING>`
**Purpose**: Template for formatting how entries appear in the results list
- **Channel Mode**: Overrides the channel's display format
- **Ad-hoc Mode**: Customize how entries are shown (default: entries as-is)
- **Requires**: `--source-command` (in ad-hoc mode)
- **Example**: `--source-display "{split:/:-1} ({split:/:0..-1|join:-})"`
#### `--source-output <STRING>`
**Purpose**: Template for formatting the final output when an entry is selected
- **Channel Mode**: Overrides the channel's output format
- **Ad-hoc Mode**: Customize what gets returned (default: entries as-is)
- **Requires**: `--source-command` (in ad-hoc mode)
- **Example**: `--source-output "code {}"`
### 👁️ Preview Options
#### `-p, --preview-command <STRING>`
**Purpose**: Command to generate preview content for the selected entry
- **Channel Mode**: Overrides the channel's preview command
- **Ad-hoc Mode**: Enables preview functionality with the specified command
- **Requires**: `--source-command` (in ad-hoc mode)
- **Conflicts**: Cannot use with `--no-preview`
- **Example**: `--preview-command "bat -n --color=always {}"`
#### `--preview-header <STRING>`
**Purpose**: Template for text displayed above the preview panel
- **Both Modes**: Sets custom header text
- **Requires**: `--preview-command` (in ad-hoc mode)
- **Conflicts**: Cannot use with `--no-preview`
- **Example**: `--preview-header "File: {split:/:-1|upper}"`
#### `--preview-footer <STRING>`
**Purpose**: Template for text displayed below the preview panel
- **Both Modes**: Sets custom footer text
- **Requires**: `--preview-command` (in ad-hoc mode)
- **Conflicts**: Cannot use with `--no-preview`
#### `--preview-offset <STRING>`
**Purpose**: Template that determines the scroll position in the preview
- **Both Modes**: Controls where preview content starts displaying
- **Requires**: `--preview-command` (in ad-hoc mode)
- **Conflicts**: Cannot use with `--no-preview`
- **Example**: `--preview-offset "10"` (start at line 10)
#### `--preview-size <INTEGER>`
**Purpose**: Width of the preview panel as a percentage
- **Both Modes**: Controls preview panel size
- **Default**: 50% of screen width
- **Range**: 1-99
- **Conflicts**: Cannot use with `--no-preview`
#### `--no-preview`
**Purpose**: Completely disables the preview panel
- **Both Modes**: Turns off all preview functionality
- **Conflicts**: Cannot use with any `--preview-*` flags
- **Use Case**: Faster performance, simpler interface
### 🎨 Interface and Layout Options
#### `--layout <LAYOUT>`
**Purpose**: Controls the overall interface orientation
- **Channel Mode**: Overrides channel's layout setting
- **Ad-hoc Mode**: Sets interface layout
- **Values**: `landscape` (side-by-side), `portrait` (stacked)
- **Default**: `landscape`
#### `--input-header <STRING>`
**Purpose**: Template for text displayed above the input field
- **Channel Mode**: Overrides channel's input header
- **Ad-hoc Mode**: Sets custom input header
- **Default**: Channel name (channel mode) or empty (ad-hoc mode)
- **Example**: `--input-header "Search files:"`
#### `--ui-scale <INTEGER>`
**Purpose**: Scales the entire interface size
- **Both Modes**: Adjusts display size as a percentage
- **Default**: 100%
- **Range**: 10-100%
- **Use Case**: Adapt to different screen sizes or preferences
#### `--no-help`
**Purpose**: Hides the help panel showing keyboard shortcuts
- **Both Modes**: Removes help information from display
- **Use Case**: More screen space, cleaner interface for experienced users
#### `--no-remote`
**Purpose**: Hides the remote control panel
- **Both Modes**: Removes remote control information from display
- **Use Case**: Simpler interface when remote features aren't needed
### ⌨️ Input and Interaction Options
#### `-i, --input <STRING>`
**Purpose**: Pre-fills the input prompt with specified text
- **Both Modes**: Starts with text already in the search box
- **Use Case**: Continue a previous search or provide default query
- **Example**: `-i "main.py"`
#### `-k, --keybindings <STRING>`
**Purpose**: Overrides default keyboard shortcuts
- **Both Modes**: Customizes key bindings for actions
- **Format**: `action1=["key1","key2"];action2=["key3"]`
- **Example**: `-k 'quit=["q","esc"];select=["enter","space"]'`
#### `--exact`
**Purpose**: Changes matching behavior from fuzzy to exact substring matching
- **Channel Mode**: Overrides channel's matching mode
- **Ad-hoc Mode**: Enables exact matching
- **Default**: Fuzzy matching
- **Use Case**: When you need precise substring matches
### ⚡ Selection Behavior Options
> **Note**: These options are mutually exclusive - only one can be used at a time.
#### `--select-1`
**Purpose**: Automatically selects and returns the entry if only one is found
- **Both Modes**: Bypasses interactive selection when there's only one match
- **Use Case**: Scripting scenarios where single results should be auto-selected
#### `--take-1`
**Purpose**: Takes the first entry after loading completes
- **Both Modes**: Automatically selects first item once all entries are loaded
- **Use Case**: Scripts that always want the first/best result
#### `--take-1-fast`
**Purpose**: Takes the first entry immediately as it appears
- **Both Modes**: Selects first item as soon as it's available
- **Use Case**: Maximum speed scripts that don't care about all options
### ⚙️ Performance and Monitoring Options
#### `-t, --tick-rate <FLOAT>`
**Purpose**: Controls how frequently the interface updates (times per second)
- **Both Modes**: Sets UI refresh rate
- **Default**: Auto-calculated based on system performance
- **Validation**: Must be positive number
- **Example**: `--tick-rate 30` (30 updates per second)
#### `--watch <FLOAT>`
**Purpose**: Automatically re-runs the source command at regular intervals
- **Channel Mode**: Overrides channel's watch interval
- **Ad-hoc Mode**: Enables live monitoring mode
- **Default**: 0 (disabled)
- **Units**: Seconds between updates
- **Conflicts**: Cannot use with selection options (`--select-1`, `--take-1`, `--take-1-fast`)
- **Example**: `--watch 2.0` (update every 2 seconds)
### 📁 Directory and Configuration Options
#### `[PATH]` (Positional Argument 2)
**Purpose**: Sets the working directory for the command
- **Both Modes**: Changes to specified directory before running
- **Default**: Current directory
- **Example**: `tv files /home/user/projects`
#### `--config-file <PATH>`
**Purpose**: Uses a custom configuration file instead of the default
- **Both Modes**: Loads settings from specified file
- **Default**: `~/.config/tv/config.toml` (Linux/macOS) or `%APPDATA%\tv\config.toml` (Windows)
- **Use Case**: Multiple configurations for different workflows
#### `--cable-dir <PATH>`
**Purpose**: Uses a custom directory for channel definitions
- **Both Modes**: Loads channels from specified directory
- **Default**: `~/.config/tv/cable/` (Linux/macOS) or `%APPDATA%\tv\cable\` (Windows)
- **Use Case**: Custom channel collections or shared team channels
### 🔧 Special Mode Options
#### `--autocomplete-prompt <STRING>`
**Purpose**: ⚡ **Activates Channel Mode** - Auto-detects channel from shell command
- **Effect**: Switches to Channel Mode automatically
- **Behavior**: Analyzes the provided command to determine appropriate channel
- **Conflicts**: Cannot use with `[CHANNEL]` positional argument
- **Use Case**: Shell integration and smart channel detection
- **Example**: `--autocomplete-prompt "git log --oneline"`
## Subcommands
### `list-channels`
Lists all available channels in the cable directory.
```bash
tv list-channels
```
### `init <SHELL>`
Generates shell completion script for the specified shell.
**Supported shells**: `bash`, `zsh`, `fish`, `powershell`, `cmd`
```bash
tv init zsh > ~/.zshrc.d/tv-completion.zsh
```
### `update-channels`
Downloads the latest channel prototypes from GitHub.
```bash
tv update-channels
```
## Usage Rules and Restrictions
> **Note**: Detailed requirements and conflicts for each flag are covered in the [Options](#options) section above. This section provides a high-level overview of the key rules.
### 🎯 Ad-hoc Mode Requirements
When using Television without a channel, certain flags become mandatory:
- **`--source-command` is required** - without this, no entries will be generated
- **Preview dependencies** - all `--preview-*` flags require `--preview-command` to be functional
- **Source formatting dependencies** - `--source-display` and `--source-output` require `--source-command`
### 🚫 Mutually Exclusive Options
These option groups cannot be used together:
- **Selection behavior**: Only one of `--select-1`, `--take-1`, or `--take-1-fast`
- **Preview control**: `--no-preview` conflicts with all `--preview-*` flags
- **Channel selection**: Cannot use both `[CHANNEL]` argument and `--autocomplete-prompt`
- **Watch vs selection**: `--watch` cannot be used with auto-selection flags
### ✅ Channel Mode Benefits
Channels provide sensible defaults, making the tool more flexible:
- Preview and source flags work independently (channel provides missing pieces)
- All UI options have reasonable defaults
- Less strict validation since channels fill in the gaps
## Configuration
### ⚡ Configuration Priority
Television uses a layered configuration system where each layer can override the previous:
1. **CLI flags** - Highest priority, overrides everything
2. **Channel configuration** - Channel-specific settings
3. **User config file** - Personal preferences
4. **Built-in defaults** - Fallback values
### 📁 Configuration Locations
#### User Configuration File
- **Linux/macOS**: `~/.config/tv/config.toml`
- **Windows**: `%APPDATA%\tv\config.toml`
#### Channel Definitions (Cable Directory)
- **Linux/macOS**: `~/.config/tv/cable/`
- **Windows**: `%APPDATA%\tv\cable\`
> **Tip**: Use `--config-file` and `--cable-dir` flags to override these default locations
## Template System
Television uses a powerful template system for dynamic content generation. Templates are enclosed in curly braces `{}` and support complex operations.
### Template-Enabled Flags
| Flag Category | Flags Using Templates |
|---------------|----------------------|
| **Source** | `--source-command`, `--source-display`, `--source-output` |
| **Preview** | `--preview-command`, `--preview-offset` |
| **Headers** | `--input-header`, `--preview-header`, `--preview-footer` |
### Basic Template Syntax
Templates support a wide range of operations that can be chained together:
```text
{operation1|operation2|operation3}
```
### Core Template Operations
| Operation | Description | Example |
|-----------|-------------|---------|
| `{}` | Full entry (passthrough) | `{}` → original entry |
| `{split:SEPARATOR:RANGE}` | Split text and extract parts | `{split:/:1}` → last path component |
| `{upper}` | Convert to uppercase | `{upper}` → "HELLO" |
| `{lower}` | Convert to lowercase | `{lower}` → "hello" |
| `{trim}` | Remove whitespace | `{trim}` → "text" |
| `{append:TEXT}` | Add text to end | `{append:.txt}` → "file.txt" |
| `{prepend:TEXT}` | Add text to beginning | `{prepend:/home/}` → "/home/file" |
### Advanced Template Operations
| Operation | Description | Example |
|-----------|-------------|---------|
| `{replace:s/PATTERN/REPLACEMENT/FLAGS}` | Regex find and replace | `{replace:s/\\.py$/.backup/}` |
| `{regex_extract:PATTERN}` | Extract matching text | `{regex_extract:\\d+}` → extract numbers |
| `{filter:PATTERN}` | Keep items matching pattern | `{split:,:..\|filter:^test}` |
| `{sort}` | Sort list items | `{split:,:..\|sort}` |
| `{unique}` | Remove duplicates | `{split:,:..\|unique}` |
| `{join:SEPARATOR}` | Join list with separator | `{split:,:..\|join:-}` |
### Template Examples
```text
# File path manipulation
{split:/:-1} # Get filename from path
{split:/:0..-1|join:/} # Get directory from path
# Text processing
{split: :..|map:{upper}|join:_} # "hello world" → "HELLO_WORLD"
{trim|replace:s/\s+/_/g} # Replace spaces with underscores
# Data extraction
{regex_extract:@(.+)} # Extract email domain
{split:,:..|filter:^[A-Z]} # Filter items starting with uppercase
```
### Range Specifications
| Syntax | Description |
|--------|-------------|
| `N` | Single index (0-based) |
| `N..M` | Range exclusive (items N to M-1) |
| `N..=M` | Range inclusive (items N to M) |
| `N..` | From N to end |
| `..M` | From start to M-1 |
| `..` | All items |
| `-1` | Last item |
| `-N` | N-th from end |
For complete template documentation, see the [Template System Documentation](https://github.com/lalvarezt/string_pipeline/blob/main/docs/template-system.md).
## Examples
> **Note**: More detailed examples with explanations are included in each option's documentation above.
### 🎯 Quick Start Examples
#### Channel Mode (Recommended)
```bash
# Basic usage - use built-in channels
tv files # Browse files in current directory
tv git-log # Browse git commit history
tv docker-images # Browse Docker images
# Channel + customization
tv files --preview-command "bat -n --color=always {}"
tv git-log --layout portrait
```
#### Ad-hoc Mode (Custom Commands)
```bash
# Simple custom finder
tv --source-command "find . -name '*.md'"
# Live system monitoring
tv --source-command "ps aux | tail -n +2" \
--watch 1.0 \
--no-preview
```

View File

@ -4,7 +4,7 @@
.SH NAME
television \- A cross\-platform, fast and extensible general purpose fuzzy finder TUI.
.SH SYNOPSIS
\fBtelevision\fR [\fB\-\-preview\-offset\fR] [\fB\-\-no\-preview\fR] [\fB\-t\fR|\fB\-\-tick\-rate\fR] [\fB\-f\fR|\fB\-\-frame\-rate\fR] [\fB\-k\fR|\fB\-\-keybindings\fR] [\fB\-i\fR|\fB\-\-input\fR] [\fB\-\-input\-header\fR] [\fB\-\-preview\-header\fR] [\fB\-\-preview\-footer\fR] [\fB\-\-source\-command\fR] [\fB\-\-source\-display\fR] [\fB\-\-source\-output\fR] [\fB\-p\fR|\fB\-\-preview\-command\fR] [\fB\-\-layout\fR] [\fB\-\-autocomplete\-prompt\fR] [\fB\-\-exact\fR] [\fB\-\-select\-1\fR] [\fB\-\-take\-1\fR] [\fB\-\-take\-1\-fast\fR] [\fB\-\-no\-remote\fR] [\fB\-\-no\-help\fR] [\fB\-\-ui\-scale\fR] [\fB\-\-preview\-size\fR] [\fB\-\-config\-file\fR] [\fB\-\-cable\-dir\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fICHANNEL\fR] [\fIPATH\fR] [\fIsubcommands\fR]
\fBtelevision\fR [\fB\-\-preview\-offset\fR] [\fB\-\-no\-preview\fR] [\fB\-t\fR|\fB\-\-tick\-rate\fR] [\fB\-\-watch\fR] [\fB\-k\fR|\fB\-\-keybindings\fR] [\fB\-i\fR|\fB\-\-input\fR] [\fB\-\-input\-header\fR] [\fB\-\-preview\-header\fR] [\fB\-\-preview\-footer\fR] [\fB\-\-source\-command\fR] [\fB\-\-source\-display\fR] [\fB\-\-source\-output\fR] [\fB\-p\fR|\fB\-\-preview\-command\fR] [\fB\-\-layout\fR] [\fB\-\-autocomplete\-prompt\fR] [\fB\-\-exact\fR] [\fB\-\-select\-1\fR] [\fB\-\-take\-1\fR] [\fB\-\-take\-1\-fast\fR] [\fB\-\-no\-remote\fR] [\fB\-\-no\-help\fR] [\fB\-\-ui\-scale\fR] [\fB\-\-preview\-size\fR] [\fB\-\-config\-file\fR] [\fB\-\-cable\-dir\fR] [\fB\-h\fR|\fB\-\-help\fR] [\fB\-V\fR|\fB\-\-version\fR] [\fICHANNEL\fR] [\fIPATH\fR] [\fIsubcommands\fR]
.SH DESCRIPTION
A cross\-platform, fast and extensible general purpose fuzzy finder TUI.
.SH OPTIONS
@ -13,29 +13,45 @@ A cross\-platform, fast and extensible general purpose fuzzy finder TUI.
A preview line number offset template to use to scroll the preview to for each
entry.
When a channel is specified: This overrides the offset defined in the channel prototype.
When no channel is specified: This flag requires \-\-preview\-command to be set.
This template uses the same syntax as the `preview` option and will be formatted
using the currently selected entry.
.TP
\fB\-\-no\-preview\fR
Disable the preview panel entirely on startup.
This flag works identically in both channel mode and ad\-hoc mode.
When set, no preview panel will be shown regardless of channel configuration
or preview\-related flags.
.TP
\fB\-t\fR, \fB\-\-tick\-rate\fR=\fIFLOAT\fR
The application\*(Aqs tick rate.
This flag works identically in both channel mode and ad\-hoc mode.
The tick rate is the number of times the application will update per
second. This can be used to control responsiveness and CPU usage on
very slow machines or very fast ones but the default should be a good
compromise for most users.
.TP
\fB\-f\fR, \fB\-\-frame\-rate\fR=\fIFLOAT\fR
[DEPRECATED] Frame rate, i.e. number of frames to render per second.
\fB\-\-watch\fR=\fIFLOAT\fR
Watch mode: reload the source command every N seconds.
This option is deprecated and will be removed in a future release.
When a channel is specified: Overrides the watch interval defined in the channel prototype.
When no channel is specified: Sets the watch interval for the ad\-hoc channel.
When set to a positive number, the application will automatically
reload the source command at the specified interval. This is useful
for monitoring changing data sources. Set to 0 to disable (default).
.TP
\fB\-k\fR, \fB\-\-keybindings\fR=\fISTRING\fR
Keybindings to override the default keybindings.
This can be used to override the default keybindings with a custom subset
This flag works identically in both channel mode and ad\-hoc mode.
This can be used to override the default keybindings with a custom subset.
The keybindings are specified as a semicolon separated list of keybinding
expressions using the configuration file formalism.
@ -44,12 +60,17 @@ Example: `tv \-\-keybindings=\*(Aqquit="esc";select_next_entry=["down","ctrl\-j"
\fB\-i\fR, \fB\-\-input\fR=\fISTRING\fR
Input text to pass to the channel to prefill the prompt.
This flag works identically in both channel mode and ad\-hoc mode.
This can be used to provide a default value for the prompt upon
startup.
.TP
\fB\-\-input\-header\fR=\fISTRING\fR
Input field header template.
When a channel is specified: Overrides the input header defined in the channel prototype.
When no channel is specified: Sets the input header for the ad\-hoc channel.
The given value is parsed as a `MultiTemplate`. It is evaluated against
the current channel name and the resulting text is shown as the input
field title. Defaults to the current channel name when omitted.
@ -57,39 +78,53 @@ field title. Defaults to the current channel name when omitted.
\fB\-\-preview\-header\fR=\fISTRING\fR
Preview header template
When a channel is specified: This overrides the header defined in the channel prototype.
When no channel is specified: This flag requires \-\-preview\-command to be set.
The given value is parsed as a `MultiTemplate`. It is evaluated for every
entry and its result is displayed above the preview panel.
.TP
\fB\-\-preview\-footer\fR=\fISTRING\fR
Preview footer template
When a channel is specified: This overrides the footer defined in the channel prototype.
When no channel is specified: This flag requires \-\-preview\-command to be set.
The given value is parsed as a `MultiTemplate`. It is evaluated for every
entry and its result is displayed below the preview panel.
.TP
\fB\-\-source\-command\fR=\fISTRING\fR
Source command to use for the current channel.
This overrides the command defined in the channel prototype.
When a channel is specified: This overrides the command defined in the channel prototype.
When no channel is specified: This creates an ad\-hoc channel with the given command.
Example: `find . \-name \*(Aq*.rs\*(Aq`
.TP
\fB\-\-source\-display\fR=\fISTRING\fR
Source display template to use for the current channel.
This overrides the display template defined in the channel prototype.
When a channel is specified: This overrides the display template defined in the channel prototype.
When no channel is specified: This flag requires \-\-source\-command to be set.
The template is used to format each entry in the results list.
Example: `{split:/:\-1}` (show only the last path segment)
.TP
\fB\-\-source\-output\fR=\fISTRING\fR
Source output template to use for the current channel.
This overrides the output template defined in the channel prototype.
When a channel is specified: This overrides the output template defined in the channel prototype.
When no channel is specified: This flag requires \-\-source\-command to be set.
The template is used to format the final output when an entry is selected.
Example: "{}" (output the full entry)
.TP
\fB\-p\fR, \fB\-\-preview\-command\fR=\fISTRING\fR
Preview command to use for the current channel.
This overrides the preview command defined in the channel prototype.
When a channel is specified: This overrides the preview command defined in the channel prototype.
When no channel is specified: This enables preview functionality for the ad\-hoc channel.
Example: "cat {}" (where {} will be replaced with the entry)
Parts of the entry can be extracted positionally using the `delimiter`
@ -97,15 +132,24 @@ option.
Example: "echo {0} {1}" will split the entry by the delimiter and pass
the first two fields to the command.
.TP
\fB\-\-layout\fR=\fISTRING\fR
\fB\-\-layout\fR=\fILAYOUT\fR
Layout orientation for the UI.
This overrides the layout/orientation defined in the channel prototype.
When a channel is specified: Overrides the layout/orientation defined in the channel prototype.
When no channel is specified: Sets the layout orientation for the ad\-hoc channel.
Options are "landscape" or "portrait".
.br
.br
[\fIpossible values: \fRlandscape, portrait]
.TP
\fB\-\-autocomplete\-prompt\fR=\fISTRING\fR
Try to guess the channel from the provided input prompt.
This flag automatically selects channel mode by guessing the appropriate channel.
It conflicts with manually specifying a channel since it determines the channel automatically.
This can be used to automatically select a channel based on the input
prompt by using the `shell_integration` mapping in the configuration
file.
@ -113,6 +157,8 @@ file.
\fB\-\-exact\fR
Use substring matching instead of fuzzy matching.
This flag works identically in both channel mode and ad\-hoc mode.
This will use substring matching instead of fuzzy matching when
searching for entries. This is useful when the user wants to search for
an exact match instead of a fuzzy match e.g. to improve performance.
@ -121,6 +167,8 @@ an exact match instead of a fuzzy match e.g. to improve performance.
Automatically select and output the first entry if there is only one
entry.
This flag works identically in both channel mode and ad\-hoc mode.
Note that most channels stream entries asynchronously which means that
knowing if there\*(Aqs only one entry will require waiting for the channel
to finish loading first.
@ -131,6 +179,8 @@ loading times are usually very short and will go unnoticed by the user.
\fB\-\-take\-1\fR
Take the first entry from the list after the channel has finished loading.
This flag works identically in both channel mode and ad\-hoc mode.
This will wait for the channel to finish loading all entries and then
automatically select and output the first entry. Unlike `select_1`, this
will always take the first entry regardless of how many entries are available.
@ -138,6 +188,8 @@ will always take the first entry regardless of how many entries are available.
\fB\-\-take\-1\-fast\fR
Take the first entry from the list as soon as it becomes available.
This flag works identically in both channel mode and ad\-hoc mode.
This will immediately select and output the first entry as soon as it
appears in the results, without waiting for the channel to finish loading.
This is the fastest option when you just want the first result.
@ -145,6 +197,8 @@ This is the fastest option when you just want the first result.
\fB\-\-no\-remote\fR
Disable the remote control.
This flag works identically in both channel mode and ad\-hoc mode.
This will disable the remote control panel and associated actions
entirely. This is useful when the remote control is not needed or
when the user wants `tv` to run in single\-channel mode (e.g. when
@ -154,6 +208,8 @@ application).
\fB\-\-no\-help\fR
Disable the help panel.
This flag works identically in both channel mode and ad\-hoc mode.
This will disable the help panel and associated toggling actions
entirely. This is useful when the help panel is not needed or
when the user wants `tv` to run with a minimal interface (e.g. when
@ -163,19 +219,26 @@ application).
\fB\-\-ui\-scale\fR=\fIINTEGER\fR [default: 100]
Change the display size in relation to the available area.
This flag works identically in both channel mode and ad\-hoc mode.
This will crop the UI to a centered rectangle of the specified
percentage of the available area (e.g. 0.5 for 50% x 50%).
percentage of the available area.
.TP
\fB\-\-preview\-size\fR=\fIINTEGER\fR
Percentage of the screen to allocate to the preview panel (1\-99).
This value overrides any `preview_size` defined in configuration files or channel prototypes.
When a channel is specified: This overrides any `preview_size` defined in configuration files or channel prototypes.
When no channel is specified: This flag requires \-\-preview\-command to be set.
.TP
\fB\-\-config\-file\fR=\fIPATH\fR
Provide a custom configuration file to use.
This flag works identically in both channel mode and ad\-hoc mode.
.TP
\fB\-\-cable\-dir\fR=\fIPATH\fR
Provide a custom cable directory to use.
This flag works identically in both channel mode and ad\-hoc mode.
.TP
\fB\-h\fR, \fB\-\-help\fR
Print help (see a summary with \*(Aq\-h\*(Aq)
@ -186,6 +249,13 @@ Print version
[\fICHANNEL\fR]
Which channel shall we watch?
When specified: The application operates in \*(Aqchannel mode\*(Aq where the selected
channel provides the base configuration, and CLI flags act as overrides.
When omitted: The application operates in \*(Aqad\-hoc mode\*(Aq where you must provide
at least \-\-source\-command to create a custom channel. In this mode, preview
and source flags have stricter validation rules.
A list of the available channels can be displayed using the
`list\-channels` command. The channel can also be changed from within
the application.
@ -193,6 +263,8 @@ the application.
[\fIPATH\fR]
The working directory to start the application in.
This flag works identically in both channel mode and ad\-hoc mode.
This can be used to specify a different working directory for the
application to start in. This is useful when the application is
started from a different directory than the one the user wants to

View File

@ -1,55 +1,92 @@
use clap::{Parser, Subcommand, ValueEnum};
/// Television CLI arguments structure.
///
/// This CLI supports two primary modes of operation:
///
/// # Channel Mode (when `channel` is specified)
/// In this mode, the specified channel provides base configuration (source commands,
/// preview commands, UI settings, etc.) and CLI flags act as **overrides** to those defaults.
/// This mode is more permissive and allows any combination of flags since they override
/// sensible channel defaults.
///
/// # Ad-hoc Mode (when `channel` is not specified)
/// In this mode, the CLI creates a custom channel on-the-fly based on the provided flags.
/// This mode has **stricter validation** to ensure the resulting channel is functional:
/// - Source-related flags (`--source-display`, `--source-output`) require `--source-command`
/// - Preview-related flags (`--preview-*`) require `--preview-command`
///
/// This validation prevents creating broken ad-hoc channels that reference non-existent commands.
#[allow(clippy::struct_excessive_bools)]
#[derive(Parser, Debug, Default)]
#[command(author, version, about, long_about = None)]
pub struct Cli {
/// Which channel shall we watch?
///
/// When specified: The application operates in 'channel mode' where the selected
/// channel provides the base configuration, and CLI flags act as overrides.
///
/// When omitted: The application operates in 'ad-hoc mode' where you must provide
/// at least --source-command to create a custom channel. In this mode, preview
/// and source flags have stricter validation rules.
///
/// A list of the available channels can be displayed using the
/// `list-channels` command. The channel can also be changed from within
/// the application.
#[arg(value_enum, index = 1, verbatim_doc_comment)]
#[arg(
value_enum,
index = 1,
verbatim_doc_comment,
conflicts_with = "autocomplete_prompt"
)]
pub channel: Option<String>,
/// A preview line number offset template to use to scroll the preview to for each
/// entry.
///
/// When a channel is specified: This overrides the offset defined in the channel prototype.
/// When no channel is specified: This flag requires --preview-command to be set.
///
/// This template uses the same syntax as the `preview` option and will be formatted
/// using the currently selected entry.
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
pub preview_offset: Option<String>,
/// Disable the preview panel entirely on startup.
#[arg(long, default_value = "false", verbatim_doc_comment)]
///
/// This flag works identically in both channel mode and ad-hoc mode.
/// When set, no preview panel will be shown regardless of channel configuration
/// or preview-related flags.
#[arg(long, default_value = "false", verbatim_doc_comment, conflicts_with_all = ["preview_offset", "preview_header", "preview_footer", "preview_size", "preview_command"])]
pub no_preview: bool,
/// The application's tick rate.
///
/// This flag works identically in both channel mode and ad-hoc mode.
///
/// The tick rate is the number of times the application will update per
/// second. This can be used to control responsiveness and CPU usage on
/// very slow machines or very fast ones but the default should be a good
/// compromise for most users.
#[arg(short, long, value_name = "FLOAT", verbatim_doc_comment)]
#[arg(short, long, value_name = "FLOAT", verbatim_doc_comment, value_parser = validate_positive_float)]
pub tick_rate: Option<f64>,
/// Watch mode: reload the source command every N seconds.
///
/// When a channel is specified: Overrides the watch interval defined in the channel prototype.
/// When no channel is specified: Sets the watch interval for the ad-hoc channel.
///
/// When set to a positive number, the application will automatically
/// reload the source command at the specified interval. This is useful
/// for monitoring changing data sources. Set to 0 to disable (default).
#[arg(long, value_name = "FLOAT", verbatim_doc_comment)]
#[arg(long, value_name = "FLOAT", verbatim_doc_comment, value_parser = validate_non_negative_float, conflicts_with_all = ["select_1", "take_1", "take_1_fast"])]
pub watch: Option<f64>,
/// [DEPRECATED] Frame rate, i.e. number of frames to render per second.
///
/// This option is deprecated and will be removed in a future release.
#[arg(short, long, value_name = "FLOAT", verbatim_doc_comment)]
pub frame_rate: Option<f64>,
/// Keybindings to override the default keybindings.
///
/// This can be used to override the default keybindings with a custom subset
/// This flag works identically in both channel mode and ad-hoc mode.
///
/// This can be used to override the default keybindings with a custom subset.
/// The keybindings are specified as a semicolon separated list of keybinding
/// expressions using the configuration file formalism.
///
@ -59,6 +96,8 @@ pub struct Cli {
/// Input text to pass to the channel to prefill the prompt.
///
/// This flag works identically in both channel mode and ad-hoc mode.
///
/// This can be used to provide a default value for the prompt upon
/// startup.
#[arg(short, long, value_name = "STRING", verbatim_doc_comment)]
@ -66,6 +105,9 @@ pub struct Cli {
/// Input field header template.
///
/// When a channel is specified: Overrides the input header defined in the channel prototype.
/// When no channel is specified: Sets the input header for the ad-hoc channel.
///
/// The given value is parsed as a `MultiTemplate`. It is evaluated against
/// the current channel name and the resulting text is shown as the input
/// field title. Defaults to the current channel name when omitted.
@ -74,29 +116,39 @@ pub struct Cli {
/// Preview header template
///
/// When a channel is specified: This overrides the header defined in the channel prototype.
/// When no channel is specified: This flag requires --preview-command to be set.
///
/// The given value is parsed as a `MultiTemplate`. It is evaluated for every
/// entry and its result is displayed above the preview panel.
#[arg(
long = "preview-header",
value_name = "STRING",
verbatim_doc_comment
verbatim_doc_comment,
conflicts_with = "no_preview"
)]
pub preview_header: Option<String>,
/// Preview footer template
///
/// When a channel is specified: This overrides the footer defined in the channel prototype.
/// When no channel is specified: This flag requires --preview-command to be set.
///
/// The given value is parsed as a `MultiTemplate`. It is evaluated for every
/// entry and its result is displayed below the preview panel.
#[arg(
long = "preview-footer",
value_name = "STRING",
verbatim_doc_comment
verbatim_doc_comment,
conflicts_with = "no_preview"
)]
pub preview_footer: Option<String>,
/// Source command to use for the current channel.
///
/// This overrides the command defined in the channel prototype.
/// When a channel is specified: This overrides the command defined in the channel prototype.
/// When no channel is specified: This creates an ad-hoc channel with the given command.
///
/// Example: `find . -name '*.rs'`
#[arg(
long = "source-command",
@ -107,7 +159,9 @@ pub struct Cli {
/// Source display template to use for the current channel.
///
/// This overrides the display template defined in the channel prototype.
/// When a channel is specified: This overrides the display template defined in the channel prototype.
/// When no channel is specified: This flag requires --source-command to be set.
///
/// The template is used to format each entry in the results list.
/// Example: `{split:/:-1}` (show only the last path segment)
#[arg(
@ -119,7 +173,9 @@ pub struct Cli {
/// Source output template to use for the current channel.
///
/// This overrides the output template defined in the channel prototype.
/// When a channel is specified: This overrides the output template defined in the channel prototype.
/// When no channel is specified: This flag requires --source-command to be set.
///
/// The template is used to format the final output when an entry is selected.
/// Example: "{}" (output the full entry)
#[arg(
@ -131,7 +187,9 @@ pub struct Cli {
/// Preview command to use for the current channel.
///
/// This overrides the preview command defined in the channel prototype.
/// When a channel is specified: This overrides the preview command defined in the channel prototype.
/// When no channel is specified: This enables preview functionality for the ad-hoc channel.
///
/// Example: "cat {}" (where {} will be replaced with the entry)
///
/// Parts of the entry can be extracted positionally using the `delimiter`
@ -142,19 +200,24 @@ pub struct Cli {
short,
long = "preview-command",
value_name = "STRING",
verbatim_doc_comment
verbatim_doc_comment,
conflicts_with = "no_preview"
)]
pub preview_command: Option<String>,
/// Layout orientation for the UI.
///
/// This overrides the layout/orientation defined in the channel prototype.
/// When a channel is specified: Overrides the layout/orientation defined in the channel prototype.
/// When no channel is specified: Sets the layout orientation for the ad-hoc channel.
///
/// Options are "landscape" or "portrait".
#[arg(long = "layout", value_enum, verbatim_doc_comment)]
pub layout: Option<LayoutOrientation>,
/// The working directory to start the application in.
///
/// This flag works identically in both channel mode and ad-hoc mode.
///
/// This can be used to specify a different working directory for the
/// application to start in. This is useful when the application is
/// started from a different directory than the one the user wants to
@ -164,14 +227,24 @@ pub struct Cli {
/// Try to guess the channel from the provided input prompt.
///
/// This flag automatically selects channel mode by guessing the appropriate channel.
/// It conflicts with manually specifying a channel since it determines the channel automatically.
///
/// This can be used to automatically select a channel based on the input
/// prompt by using the `shell_integration` mapping in the configuration
/// file.
#[arg(long, value_name = "STRING", verbatim_doc_comment)]
#[arg(
long,
value_name = "STRING",
verbatim_doc_comment,
conflicts_with = "channel"
)]
pub autocomplete_prompt: Option<String>,
/// Use substring matching instead of fuzzy matching.
///
/// This flag works identically in both channel mode and ad-hoc mode.
///
/// This will use substring matching instead of fuzzy matching when
/// searching for entries. This is useful when the user wants to search for
/// an exact match instead of a fuzzy match e.g. to improve performance.
@ -181,6 +254,8 @@ pub struct Cli {
/// Automatically select and output the first entry if there is only one
/// entry.
///
/// This flag works identically in both channel mode and ad-hoc mode.
///
/// Note that most channels stream entries asynchronously which means that
/// knowing if there's only one entry will require waiting for the channel
/// to finish loading first.
@ -197,6 +272,8 @@ pub struct Cli {
/// Take the first entry from the list after the channel has finished loading.
///
/// This flag works identically in both channel mode and ad-hoc mode.
///
/// This will wait for the channel to finish loading all entries and then
/// automatically select and output the first entry. Unlike `select_1`, this
/// will always take the first entry regardless of how many entries are available.
@ -210,6 +287,8 @@ pub struct Cli {
/// Take the first entry from the list as soon as it becomes available.
///
/// This flag works identically in both channel mode and ad-hoc mode.
///
/// This will immediately select and output the first entry as soon as it
/// appears in the results, without waiting for the channel to finish loading.
/// This is the fastest option when you just want the first result.
@ -223,6 +302,8 @@ pub struct Cli {
/// Disable the remote control.
///
/// This flag works identically in both channel mode and ad-hoc mode.
///
/// This will disable the remote control panel and associated actions
/// entirely. This is useful when the remote control is not needed or
/// when the user wants `tv` to run in single-channel mode (e.g. when
@ -233,6 +314,8 @@ pub struct Cli {
/// Disable the help panel.
///
/// This flag works identically in both channel mode and ad-hoc mode.
///
/// This will disable the help panel and associated toggling actions
/// entirely. This is useful when the help panel is not needed or
/// when the user wants `tv` to run with a minimal interface (e.g. when
@ -243,28 +326,36 @@ pub struct Cli {
/// Change the display size in relation to the available area.
///
/// This flag works identically in both channel mode and ad-hoc mode.
///
/// This will crop the UI to a centered rectangle of the specified
/// percentage of the available area (e.g. 0.5 for 50% x 50%).
/// percentage of the available area.
#[arg(
long,
value_name = "INTEGER",
default_value = "100",
verbatim_doc_comment
verbatim_doc_comment,
value_parser = clap::value_parser!(u16).range(10..=100)
)]
pub ui_scale: u16,
/// Percentage of the screen to allocate to the preview panel (1-99).
///
/// This value overrides any `preview_size` defined in configuration files or channel prototypes.
#[arg(long, value_name = "INTEGER", verbatim_doc_comment)]
/// When a channel is specified: This overrides any `preview_size` defined in configuration files or channel prototypes.
/// When no channel is specified: This flag requires --preview-command to be set.
#[arg(long, value_name = "INTEGER", verbatim_doc_comment, value_parser = clap::value_parser!(u16).range(1..=99), conflicts_with = "no_preview")]
pub preview_size: Option<u16>,
/// Provide a custom configuration file to use.
#[arg(long, value_name = "PATH", verbatim_doc_comment)]
///
/// This flag works identically in both channel mode and ad-hoc mode.
#[arg(long, value_name = "PATH", verbatim_doc_comment, value_parser = validate_file_path)]
pub config_file: Option<String>,
/// Provide a custom cable directory to use.
#[arg(long, value_name = "PATH", verbatim_doc_comment)]
///
/// This flag works identically in both channel mode and ad-hoc mode.
#[arg(long, value_name = "PATH", verbatim_doc_comment, value_parser = validate_directory_path)]
pub cable_dir: Option<String>,
#[command(subcommand)]
@ -302,3 +393,40 @@ pub enum LayoutOrientation {
Landscape,
Portrait,
}
// Add validator functions
fn validate_positive_float(s: &str) -> Result<f64, String> {
match s.parse::<f64>() {
Ok(val) if val > 0.0 => Ok(val),
Ok(_) => Err("Value must be positive".to_string()),
Err(_) => Err("Invalid number format".to_string()),
}
}
fn validate_non_negative_float(s: &str) -> Result<f64, String> {
match s.parse::<f64>() {
Ok(val) if val >= 0.0 => Ok(val),
Ok(_) => Err("Value must be non-negative".to_string()),
Err(_) => Err("Invalid number format".to_string()),
}
}
fn validate_file_path(s: &str) -> Result<String, String> {
use std::path::Path;
let path = Path::new(s);
if path.exists() && path.is_file() {
Ok(s.to_string())
} else {
Err(format!("File does not exist: {}", s))
}
}
fn validate_directory_path(s: &str) -> Result<String, String> {
use std::path::Path;
let path = Path::new(s);
if path.exists() && path.is_dir() {
Ok(s.to_string())
} else {
Err(format!("Directory does not exist: {}", s))
}
}

View File

@ -18,6 +18,27 @@ use crate::{
pub mod args;
/// # CLI Use Cases
///
/// The CLI interface supports two primary use cases:
///
/// ## 1. Channel-based mode (channel is specified)
/// When a channel is provided, the CLI operates in **override mode**:
/// - The channel provides the base configuration (source, preview, UI settings)
/// - All CLI flags act as **overrides** to the channel's defaults
/// - Most restrictions are enforced at the clap level using `conflicts_with`
/// - Templates and keybindings are validated after clap parsing
/// - More permissive - allows any combination of flags as they override channel defaults
///
/// ## 2. Ad-hoc mode (no channel specified)
/// When no channel is provided, the CLI creates an **ad-hoc channel**:
/// - Stricter validation rules apply for interdependent flags
/// - `--preview-*` flags require `--preview-command` to be set
/// - `--source-*` flags require `--source-command` to be set
/// - This ensures the ad-hoc channel has all necessary components to function
///
/// The validation logic in `post_process()` enforces these constraints for ad-hoc mode
/// while allowing full flexibility in channel-based mode.
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub struct PostProcessedCli {
@ -26,7 +47,7 @@ pub struct PostProcessedCli {
pub source_command_override: Option<Template>,
pub source_display_override: Option<Template>,
pub source_output_override: Option<Template>,
pub working_directory: Option<String>,
pub working_directory: Option<PathBuf>,
pub autocomplete_prompt: Option<String>,
// Preview configuration
@ -56,7 +77,6 @@ pub struct PostProcessedCli {
// Performance configuration
pub tick_rate: Option<f64>,
pub frame_rate: Option<f64>,
pub watch_interval: Option<f64>,
// Configuration sources
@ -105,7 +125,6 @@ impl Default for PostProcessedCli {
// Performance configuration
tick_rate: None,
frame_rate: None,
watch_interval: None,
// Configuration sources
@ -118,6 +137,20 @@ impl Default for PostProcessedCli {
}
}
/// Post-processes the raw CLI arguments into a structured format with validation.
///
/// This function handles the two main CLI use cases:
///
/// **Channel-based mode**: When `cli.channel` is provided, all flags are treated as
/// overrides to the channel's configuration. Validation is minimal since the channel
/// provides sensible defaults.
///
/// **Ad-hoc mode**: When no channel is specified, stricter validation ensures that
/// interdependent flags are used correctly:
/// - Preview flags (`--preview-offset`, `--preview-size`, etc.) require `--preview-command`
/// - Source flags (`--source-display`, `--source-output`) require `--source-command`
///
/// This prevents creating broken ad-hoc channels that reference non-existent commands.
pub fn post_process(cli: Cli) -> PostProcessedCli {
// Parse literal keybindings passed through the CLI
let keybindings = cli.keybindings.as_ref().map(|kb| {
@ -144,13 +177,20 @@ pub fn post_process(cli: Cli) -> PostProcessedCli {
})
});
// Validate interdependent flags for ad-hoc mode (when no channel is specified)
// This ensures ad-hoc channels have all necessary components to function properly
validate_adhoc_mode_constraints(&cli);
// Determine channel and working_directory
let (channel, working_directory) = match &cli.channel {
Some(c) if Path::new(c).exists() => {
// If the channel is a path, use it as the working directory
(None, Some(c.clone()))
(None, Some(PathBuf::from(c)))
}
_ => (cli.channel.clone(), cli.working_directory.clone()),
_ => (
cli.channel.clone(),
cli.working_directory.as_ref().map(PathBuf::from),
),
};
// Parse source overrides if any source fields are provided
@ -224,18 +264,71 @@ pub fn post_process(cli: Cli) -> PostProcessedCli {
// Performance configuration
tick_rate: cli.tick_rate,
frame_rate: cli.frame_rate,
watch_interval: cli.watch,
// Configuration sources
config_file: cli.config_file.map(expand_tilde),
cable_dir: cli.cable_dir.map(expand_tilde),
config_file: cli.config_file.map(|p| expand_tilde(&p)),
cable_dir: cli.cable_dir.map(|p| expand_tilde(&p)),
// Command handling
command: cli.command,
}
}
/// Validates interdependent flags when operating in ad-hoc mode (no channel specified).
///
/// In ad-hoc mode, certain flags require their corresponding command to be specified:
/// - Source-related flags (`--source-display`, `--source-output`) require `--source-command`
/// - Preview-related flags (`--preview-offset`, `--preview-size`, etc.) require `--preview-command`
///
/// This validation ensures that ad-hoc channels have all necessary components to function.
/// When a channel is specified, these validations are skipped as the channel provides defaults.
fn validate_adhoc_mode_constraints(cli: &Cli) {
// Skip validation if a channel is specified (channel-based mode)
if cli.channel.is_some() {
return;
}
// Validate source-related flags in ad-hoc mode
if cli.source_command.is_none() {
let source_flags = [
("--source-display", cli.source_display.is_some()),
("--source-output", cli.source_output.is_some()),
("--preview-command", cli.preview_command.is_some()),
];
for (flag_name, is_set) in source_flags {
if is_set {
cli_parsing_error_exit(&format!(
"{} requires a source command when no channel is specified. \
Either specify a channel (which may have its own source command) or provide --source-command.",
flag_name
));
}
}
}
// Validate preview-related flags in ad-hoc mode
if cli.preview_command.is_none() {
let preview_flags = [
("--preview-offset", cli.preview_offset.is_some()),
("--preview-size", cli.preview_size.is_some()),
("--preview-header", cli.preview_header.is_some()),
("--preview-footer", cli.preview_footer.is_some()),
];
for (flag_name, is_set) in preview_flags {
if is_set {
cli_parsing_error_exit(&format!(
"{} requires a preview command when no channel is specified. \
Either specify a channel (which may have its own preview command) or provide --preview-command.",
flag_name
));
}
}
}
}
fn cli_parsing_error_exit(message: &str) -> ! {
eprintln!("Error parsing CLI arguments: {message}\n");
std::process::exit(1);
@ -369,7 +462,7 @@ pub fn version() -> String {
|`-----------' |/
~~~~~~~~~~~~~~~
__ __ _ _
/ /____ / /__ _ __(_)__ (_)__ ___
/ /____ / /__ _ __(_)__ (_)__ ___
/ __/ -_) / -_) |/ / (_-</ / _ \\/ _ \\
\\__/\\__/_/\\__/|___/_/___/_/\\___/_//_/
@ -403,10 +496,9 @@ mod tests {
"bat -n --color=always {}".to_string(),
);
assert_eq!(post_processed_cli.tick_rate, None);
assert_eq!(post_processed_cli.frame_rate, None);
assert_eq!(
post_processed_cli.working_directory,
Some("/home/user".to_string())
Some(PathBuf::from("/home/user"))
);
}
@ -422,7 +514,7 @@ mod tests {
assert_eq!(
post_processed_cli.working_directory,
Some(".".to_string())
Some(PathBuf::from("."))
);
assert_eq!(post_processed_cli.command, None);
}

View File

@ -37,8 +37,6 @@ pub struct AppConfig {
pub config_dir: PathBuf,
#[serde(default = "default_cable_dir")]
pub cable_dir: PathBuf,
#[serde(default = "default_frame_rate")]
pub frame_rate: f64,
#[serde(default = "default_tick_rate")]
pub tick_rate: f64,
/// The default channel to use when no channel is specified
@ -54,7 +52,6 @@ impl Hash for AppConfig {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.data_dir.hash(state);
self.config_dir.hash(state);
self.frame_rate.to_bits().hash(state);
self.tick_rate.to_bits().hash(state);
}
}
@ -314,10 +311,6 @@ fn project_directory() -> Option<ProjectDirs> {
ProjectDirs::from("com", "", env!("CARGO_PKG_NAME"))
}
fn default_frame_rate() -> f64 {
60.0
}
pub fn default_tick_rate() -> f64 {
50.0
}
@ -393,8 +386,6 @@ mod tests {
}
const USER_CONFIG_1: &str = r#"
frame_rate = 30.0
[ui]
ui_scale = 40
theme = "television"
@ -431,7 +422,6 @@ mod tests {
let mut default_config: Config =
toml::from_str(DEFAULT_CONFIG).unwrap();
default_config.application.frame_rate = 30.0;
default_config.ui.ui_scale = 40;
default_config.ui.theme = "television".to_string();
default_config.keybindings.extend({

View File

@ -1,6 +1,6 @@
use std::env;
use std::io::{BufWriter, IsTerminal, Write, stdout};
use std::path::Path;
use std::path::PathBuf;
use std::process::exit;
use anyhow::Result;
@ -16,7 +16,7 @@ use television::{
},
utils::clipboard::CLIPBOARD,
};
use tracing::{debug, error, info};
use tracing::{debug, info};
use television::app::{App, AppOptions};
use television::cli::{
@ -66,7 +66,9 @@ async fn main() -> Result<()> {
load_cable(&config.application.cable_dir).unwrap_or_else(|| exit(1));
// optionally change the working directory
args.working_directory.as_ref().map(set_current_dir);
if let Some(ref working_dir) = args.working_directory {
set_current_dir(working_dir)?;
}
// determine the channel to use based on the CLI arguments and configuration
debug!("Determining channel...");
@ -165,16 +167,7 @@ fn apply_cli_overrides(args: &PostProcessedCli, config: &mut Config) {
}
}
pub fn set_current_dir(path: &String) -> Result<()> {
let path = Path::new(path);
if !path.exists() {
error!("Working directory \"{}\" does not exist", path.display());
println!(
"Error: Working directory \"{}\" does not exist",
path.display()
);
exit(1);
}
pub fn set_current_dir(path: &PathBuf) -> Result<()> {
env::set_current_dir(path)?;
Ok(())
}
@ -240,14 +233,14 @@ pub fn determine_channel(
debug!("Creating ad-hoc channel with source command override");
let source_cmd = args.source_command_override.as_ref().unwrap();
// Create an ad-hoc channel prototype with hidden UI elements
// Create an ad-hoc channel prototype
let mut prototype = ChannelPrototype::new("custom", source_cmd.raw());
// Set UI spec to hide preview and help, and set input header to "Custom Channel"
// Set UI spec - only hide preview if no preview command is provided
prototype.ui = Some(UiSpec {
ui_scale: None,
show_help_bar: Some(false),
show_preview_panel: Some(false),
show_preview_panel: Some(args.preview_command_override.is_some()),
orientation: None,
input_bar_position: None,
preview_size: None,
@ -307,12 +300,6 @@ pub fn determine_channel(
if let Some(preview_offset) = &args.preview_offset_override {
if let Some(ref mut preview) = channel_prototype.preview {
preview.offset = Some(preview_offset.clone());
} else {
// Cannot set offset without a preview command
eprintln!(
"Error: Cannot set preview offset without a preview command"
);
std::process::exit(1);
}
}