Chat about this codebase

AI-powered code exploration

Online

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 options

  • Cutting & Trimming
    • Specify in/out points interactively or via CLI flags
    • Lossless segment extraction using ffmpeg’s copy codec
    • Batch mode for scripted trimming

  • Marker 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 conversion

  • chafa Bindings
    • Convert raw pixel buffers into ANSI sequences
    • Support truecolor, eight‐color, and grayscale modes

  • TUI (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

  1. Launch player at 100 columns:
    vic movie.mp4 --width 100
    
  2. Play until your start point; press i
  3. Continue to end point; press o
  4. Press r, enter scene1.mp4 at prompt
  5. 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:

  1. Video Decoding: Spawns FFmpeg to output raw RGB frames.
  2. ANSI Rendering: Uses libchafa via Rust FFI to convert pixels into ANSI art.
  3. Terminal UI: Implements an MVU‐style TUI using Crossterm for user interaction and marker management.

Key flow:

  1. FrameIterator spawns FFmpeg → streams RGB24 frames.
  2. chafa::Canvas ingests pixel buffers → builds ANSI strings.
  3. 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 use pkg_config::Config.
  • After build, include!(concat!(env!("OUT_DIR"), "/bindings.rs")); lives in chafa-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 into ROADMAP.md.

Use these tools to streamline local development and long-term planning.