Project Overview
vic is a terminal-native video player and cutter built in Rust. It combines ffmpeg for video decoding and segment trimming with chafa for ANSI‐art rendering, all wrapped in a Model–View–Update TUI using crossterm. Developers and power users leverage vic to preview, annotate, mark, and extract clips directly from the command line.
Main Capabilities
Playback
• Decode and display video in ANSI art at configurable width
• Standard controls: Play, pause, seek, frame‐step
• Adjust frame rate, scaling, and color optionsCutting & Trimming
• Specify in/out points interactively or via CLI flags
• Lossless segment extraction using ffmpeg’s copy codec
• Batch mode for scripted trimmingMarker Management
• Set, navigate, and clear named markers during playback
• Export marker lists for downstream editing
• Jump directly to markers for rapid review
Architecture Highlights
ffmpeg Integration
• Spawn ffmpeg subprocesses for decoding and cutting
• Stream raw frames to chafa for on‐the‐fly conversionchafa Bindings
• Convert raw pixel buffers into ANSI sequences
• Support truecolor, eight‐color, and grayscale modesTUI (MVU Pattern)
• crossterm backend for cross‐platform terminal control
• Unidirectional data flow: user events → update → render
• Built-in keybindings for playback and marker commands
When to Use vic
- Rapid video previews without a GUI
- Lightweight remote workflows over SSH
- Automated trimming pipelines in shell scripts
- Quick annotation or marker‐based review sessions
Quick CLI Examples
Run a video at 80-column width:
vic play sample.mp4 --width 80
Trim from 00:01:00 to 00:02:30:
vic cut sample.mp4 --from 00:01:00 --to 00:02:30 -o clip.mp4
Interactively set markers during playback:
vic play sample.mp4
# Press “m” to toggle markers, “n”/“p” to navigate
For full options and advanced usage, refer to the built-in help:
vic --help
## Getting Started
Follow these steps to install, build and run vic in under five minutes.
### 1. Prerequisites
Ensure you have the following tools and libraries:
- Rust toolchain (rustc 1.65+ & cargo)
- FFmpeg
- pkg-config
- GLib 2.0 development headers
- Chafa C library
- Git, Make, gaze (for live-reload)
#### Ubuntu 24.04
```bash
sudo apt-get update
sudo apt-get install -y \
build-essential \
pkg-config \
libglib2.0-dev \
libchafa-dev \
ffmpeg \
git \
make \
gaze
macOS (Homebrew)
brew update
brew install \
pkg-config \
glib \
chafa \
ffmpeg \
git \
make \
gaze
2. Clone and Build
git clone https://github.com/wong-justin/vic.git
cd vic
cargo build --release
This produces the vic
binary in target/release/vic
.
Live-reload during development
make dev
This runs gaze
to watch for source changes and rebuild automatically.
3. Install Locally (optional)
Install vic
into your Cargo bin directory:
cargo install --path .
You can now invoke vic
from anywhere.
4. Run vic
Basic playback:
vic path/to/video.mp4 --width 80 --height 24
This scales your terminal output to 80 columns × 24 rows.
Common CLI Options
vic --help
-w, --width <COLUMNS>
output columns (default: terminal width)-h, --height <ROWS>
output rows (default: terminal height)-m, --markers <FILE>
load or save edit markers-q, --quiet
suppress log messages
Example: Interactive Cut & Mark
vic video.mp4 \
--width 100 \
--height 30 \
--markers edits.json
Use spacebar to play/pause, arrow keys to seek, and m
to toggle markers. Saved markers persist in edits.json
.
5. Generate Test Media & Roadmap
- generate-tests: placeholder for test-media scripts
- roadmap: aggregate TODOs for planning
make generate-tests
make roadmap
You’re ready to explore vic’s interactive terminal-based video editing. For advanced usage and API integration, see the FrameIterator and chafa-sys sections.
Usage Guide
This section covers daily workflows for vic: launching the player, non-interactive cuts via CLI flags, and in-TUI key bindings for playback control and segment extraction.
CLI Flags
vic uses pico-args to parse command-line options. All flags are optional except the input file.
• -w, --width <COLUMNS>
Set terminal graphic width in columns. Default: auto-detect.
• -h, --height <ROWS>
Fix output height. Default: scales to preserve aspect ratio.
• -f, --fps <FPS>
Target playback framerate. Default: 10.
• -a, --audio
Enable audio playback via ffplay. Default: audio off.
• --no-audio
Disable audio (overrides --audio
).
• -s, --cut-in <TIME>
In-point for non-interactive cut. Format: seconds or HH:MM:SS
.
• -e, --cut-out <TIME>
Out-point for non-interactive cut.
• -o, --output <FILE>
Filename for extracted segment. Default: segment_<in>_<out>.mp4
.
• --segment <IN:OUT>
Shorthand for --cut-in IN --cut-out OUT
.
• --ffmpeg-args <ARGS>
Extra arguments passed to ffmpeg during cut/export.
• --chafa-args <ARGS>
Extra arguments passed to chafa for rendering.
• -V, --version
Print version and exit.
• -?, --help
Show help and exit.
CLI Examples
# Play video at 80 columns, 15 fps, with audio
vic sample.mp4 --width 80 --fps 15 --audio
# Non-interactive cut: 10–20 seconds → clip.mp4
vic sample.mp4 \
--cut-in 10 \
--cut-out 20 \
--output clip.mp4
# Use segment shorthand and pass extra ffmpeg args
vic sample.mp4 \
--segment 00:01:00:00:02:30 \
--output highlights.mp4 \
--ffmpeg-args "-c:v libx264 -crf 23"
Interactive Key Bindings
While vic runs in the terminal UI, use these keys for playback, marking, and cutting:
Playback
• Space Toggle play/pause
• ← / → Seek −1 s / +1 s
• ↑ / ↓ Seek −10 s / +10 s
Markers
• i Set “in” marker at current timestamp
• o Set “out” marker
Cut & Export
• c Cut between in/out and write to --output
(uses ffmpeg)
• r Prompt for output filename and record segment
• m Clear both markers
Misc
• ? Display on-screen help overlay
• q Quit vic
Interactive Session Example
- Launch player at 100 columns:
vic movie.mp4 --width 100
- Play until your start point; press
i
- Continue to end point; press
o
- Press
r
, enterscene1.mp4
at prompt - Wait for ffmpeg export; press
q
to quit
All TUI output renders via chafa in the alternate screen; exported segments use ffmpeg in the background.
Development & Internals
This section guides contributors through Vic’s internal architecture, the FFI bridge to libchafa, and CI/dev workflows.
1. Architecture Overview
Vic combines three core layers:
- Video Decoding: Spawns FFmpeg to output raw RGB frames.
- ANSI Rendering: Uses libchafa via Rust FFI to convert pixels into ANSI art.
- Terminal UI: Implements an MVU‐style TUI using Crossterm for user interaction and marker management.
Key flow:
FrameIterator
spawns FFmpeg → streams RGB24 frames.chafa::Canvas
ingests pixel buffers → builds ANSI strings.tui::Program
renders frames, handles input, updates markers.
2. FFI Layer (chafa-sys)
2.1 build.rs: Dynamic Linking & Bindgen
Automatically find and link libchafa, generate Rust bindings in $OUT_DIR/bindings.rs
.
// chafa-sys/build.rs
fn main() {
// Probe system-installed chafa (+ glib)
let library = pkg_config::probe_library("chafa")
.expect("chafa not found via pkg-config");
// Configure bindgen
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.derive_default(true)
.clang_args(
library.include_paths.iter()
.map(|p| format!("-I{}", p.display()))
)
.generate()
.expect("Failed to generate bindings");
// Write to OUT_DIR
bindings
.write_to_file(
std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap())
.join("bindings.rs"),
)
.expect("Couldn't write bindings.rs");
}
Practical tips:
- Install dependencies:
sudo apt install libglib2.0-dev libchafa-dev pkg-config
- On non-standard prefixes, set
PKG_CONFIG_PATH
or usepkg_config::Config
. - After build,
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
lives inchafa-sys/src/lib.rs
.
2.2 lib.rs: Exposing Bindings & Tests
// chafa-sys/src/lib.rs
#![allow(non_camel_case_types, non_snake_case, non_upper_case_globals)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_not_empty() {
let ver = unsafe { chafa_version_string() };
assert!(!ver.is_null());
}
}
Run cargo test -p chafa-sys
to verify FFI integrity.
3. Idiomatic Rust Wrappers (src/chafa.rs)
Provides safe, ergonomic types for symbol maps, canvases, and ANSI rendering.
use chafa_sys as sys;
use std::ptr::NonNull;
pub struct SymbolMap(NonNull<sys::ChafaSymbolMap>);
pub struct Config(NonNull<sys::ChafaConfig>);
pub struct Canvas(NonNull<sys::ChafaCanvas>);
impl Canvas {
/// Create a new canvas for given output cols/rows
pub fn new(cols: u16, rows: u16, config: &Config) -> Self {
let raw = unsafe { sys::chafa_canvas_new(cols.into(), rows.into(), config.0.as_ptr()) };
Canvas(NonNull::new(raw).expect("Failed to create canvas"))
}
/// Draw an RGB pixel buffer into the canvas
pub fn draw_pixels(&mut self, pixels: &[u8]) {
unsafe {
sys::chafa_canvas_load_rgb(
self.0.as_ptr(),
pixels.as_ptr(),
pixels.len().try_into().unwrap(),
);
}
}
/// Build ANSI string from current canvas state
pub fn build_ansi(&self) -> String {
let c_str = unsafe { sys::chafa_canvas_build_ansi(self.0.as_ptr()) };
let s = unsafe { std::ffi::CStr::from_ptr(c_str) }.to_string_lossy().into_owned();
unsafe { sys::chafa_free(c_str as *mut _) };
s
}
}
Common patterns:
- Configure
Config
for truecolor, symbol set, work factor. - Reuse one
Canvas
to avoid repeated allocations.
4. Terminal UI (src/tui.rs)
Implements a minimal MVU TUI:
use crossterm::{execute, terminal::{enable_raw_mode, disable_raw_mode}};
use std::io::{stdout, Write};
pub struct Program<State> {
state: State,
view: fn(&State) -> String,
update: fn(&mut State, Event),
}
impl<State> Program<State> {
pub fn run(mut self) -> crossterm::Result<()> {
enable_raw_mode()?;
loop {
// Render view
execute!(stdout(), crossterm::cursor::MoveTo(0,0))?;
print!("{}", (self.view)(&self.state));
stdout().flush()?;
// Handle input
if let Event::Key(KeyCode::Char('q')) = read()? {
break;
}
(self.update)(&mut self.state, read()?);
}
disable_raw_mode()?;
Ok(())
}
}
Usage example:
fn main() -> crossterm::Result<()> {
let init_state = AppState::default();
let program = Program {
state: init_state,
view: render_ui,
update: handle_event,
};
program.run()
}
5. CI & Development Workflows
5.1 GitHub Actions (/.github/workflows/test.yml)
Automates build/test and native libchafa compilation on Ubuntu 24.04:
name: Rust
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Install deps
run: sudo apt-get update && sudo apt-get install -y libglib2.0-dev pkg-config
- name: Build chafa
run: |
curl -sSL https://hpjansson.org/chafa/releases/chafa-1.14.4.tar.xz -O
tar xf chafa-1.14.4.tar.xz
cd chafa-1.14.4
./configure --without-tools
make -j$(nproc) && sudo make install && sudo ldconfig
- name: Cargo build & test
run: cargo test --verbose
Tips:
- Add
actions/cache
for~/.cargo/registry
to speed builds. - Use a matrix strategy to test multiple Rust toolchains.
- Update chafa version in URL and directory name when bumping.
5.2 Makefile
Common targets in project root:
.PHONY: dev generate-tests roadmap
dev:
gaze -c 'cargo build && cargo test' .
generate-tests:
@echo "Placeholder: generate test media with ffmpeg scripts"
roadmap:
@grep -R --include=*.rs "TODO" -n src/ > ROADMAP.md
make dev
: live‐reload on source changes.make generate-tests
: hook to script FFmpeg media generation.make roadmap
: aggregate TODOs intoROADMAP.md
.
Use these tools to streamline local development and long-term planning.