diff --git a/.gitignore b/.gitignore index 43337eb..9bc8e55 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ Thumbs.db # IDE .vscode/ .idea/ +.kiro/ *.swp *.swo @@ -29,4 +30,4 @@ src/vendor/ temp/ # Keep important files -!LICENSE \ No newline at end of file +!LICENSE diff --git a/CHANGELOG.md b/CHANGELOG.md index bebc6bb..780998b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.2.0] - 2024-12-10 + +### Added +- **Interactive Mode**: Step-by-step wizard for media generation + - Media type selection (video/audio) with clear prompts + - Parameter configuration with validation and default values + - Configuration summary and confirmation before generation + - Real-time progress feedback during media creation + - Robust input handling with error recovery + - Support for both experienced and novice users + +- **Enhanced User Experience** + - User-friendly prompts with clear instructions + - Input validation with helpful error messages + - Default value suggestions for quick configuration + - Graceful handling of invalid input with retry options + +### Enhanced +- **CLI Interface**: Maintained full backward compatibility +- **Error Handling**: Improved error messages and recovery +- **Cross-platform Support**: Enhanced terminal compatibility + +### Technical +- **Input System**: Redesigned input handling using std.io.getStdIn().reader() +- **Validation**: Comprehensive input validation and sanitization +- **Memory Management**: Improved memory handling for interactive sessions +- **Testing**: Added comprehensive unit and integration tests for interactive mode + ## [0.1.0] - 2025-10-11 ### Added diff --git a/README.md b/README.md index bfcfa6a..b6e24cb 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ A fast, cross-platform command-line utility for generating test media files with - šŸŽ¬ **Video generation** with animated countdown timer - šŸŽµ **Audio generation** with customizable sine wave test tones +- šŸ§™ā€ā™‚ļø **Interactive mode** - Step-by-step wizard for easy configuration - šŸŽÆ **Multiple formats** - MP4, AVI, MOV, MKV, MP3, WAV, AAC, FLAC - āš™ļø **Highly configurable** - resolution, bitrate, duration, codecs, frequency - šŸš€ **Cross-platform** - Windows, macOS, Linux (Intel & ARM) @@ -37,6 +38,14 @@ Download the latest release for your platform: ### Basic Usage +#### Interactive Mode (Recommended for New Users) +```bash +# Launch interactive wizard - guided setup with prompts +./media-gen i +``` +*Perfect for beginners or when exploring different options. The wizard guides you through each step with helpful prompts and default values.* + +#### Command Line Interface (CLI) ```bash # Generate a 10-second countdown video ./media-gen video --duration 10 --output countdown.mp4 @@ -50,6 +59,21 @@ Download the latest release for your platform: # Show help ./media-gen help ``` +*Ideal for automation, scripting, or when you know exactly what parameters you need.* + +#### When to Use Each Approach + +- **Use Interactive Mode** when: + - You're new to media-gen + - You want to explore different configuration options + - You prefer guided setup with validation + - You need help understanding available parameters + +- **Use CLI Mode** when: + - You're automating media generation + - You know the exact parameters you need + - You're writing scripts or batch operations + - You prefer direct command execution ## šŸ“– Usage Guide @@ -285,9 +309,9 @@ chmod +x media-gen ### Getting Help - šŸ“– Check this README for usage examples -- ļæ½ Breowse [EXAMPLES.md](EXAMPLES.md) for comprehensive scenarios -- ļæ½ [Reqport bugs](https://github.com/DimazzzZ/media-gen/issues) -- ļæ½ [Rtequest features](https://github.com/DimazzzZ/media-gen/issues) +- Browse [EXAMPLES.md](EXAMPLES.md) for comprehensive scenarios +- [Report bugs](https://github.com/DimazzzZ/media-gen/issues) +- [Request features](https://github.com/DimazzzZ/media-gen/issues) - šŸ’¬ [Start a discussion](https://github.com/DimazzzZ/media-gen/discussions) ## šŸ“„ License @@ -302,4 +326,4 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file --- -**Made with ā¤ļø for developers who need reliable test media files** \ No newline at end of file +**Made with ā¤ļø for developers who need reliable test media files** diff --git a/src/cli.zig b/src/cli.zig index 2531248..b0e7388 100644 --- a/src/cli.zig +++ b/src/cli.zig @@ -35,6 +35,7 @@ pub fn printHelp() !void { print("Commands:\n", .{}); print(" video Generate video file with countdown timer\n", .{}); print(" audio Generate audio file with test tones\n", .{}); + print(" i, interactive Interactive mode - guided setup\n", .{}); print(" help Show this help message\n\n", .{}); print("Video options:\n", .{}); print(" --width Video width (default: 1920)\n", .{}); @@ -66,6 +67,12 @@ pub fn parseAndExecute(allocator: std.mem.Allocator, args: [][:0]u8) !void { return; } + if (std.mem.eql(u8, args[1], "i") or std.mem.eql(u8, args[1], "interactive")) { + const interactive = @import("interactive.zig"); + try interactive.run(allocator); + return; + } + if (std.mem.eql(u8, args[1], "video")) { var config = VideoConfig{}; try parseVideoArgs(args[2..], &config); diff --git a/src/generators/audio.zig b/src/generators/audio.zig index 8d61729..69b52f3 100644 --- a/src/generators/audio.zig +++ b/src/generators/audio.zig @@ -4,6 +4,10 @@ const cli = @import("../cli.zig"); const ffmpeg = @import("../ffmpeg.zig"); pub fn generate(allocator: std.mem.Allocator, config: cli.AudioConfig) !void { + try generateWithProgress(allocator, config, false); +} + +pub fn generateWithProgress(allocator: std.mem.Allocator, config: cli.AudioConfig, show_progress: bool) !void { print("Generating audio with parameters:\n", .{}); print(" Duration: {}s\n", .{config.duration}); print(" Sample rate: {}Hz\n", .{config.sample_rate}); @@ -78,6 +82,12 @@ pub fn generate(allocator: std.mem.Allocator, config: cli.AudioConfig) !void { } // Execute FFmpeg command + if (show_progress) { + print("šŸŽµ Running FFmpeg encoder...\n", .{}); + print("ā³ Please wait, encoding {d}s audio...\n", .{config.duration}); + } + + // Run FFmpeg const result = std.process.Child.run(.{ .allocator = allocator, .argv = cmd_args, diff --git a/src/generators/video.zig b/src/generators/video.zig index b89e0c2..daf3bec 100644 --- a/src/generators/video.zig +++ b/src/generators/video.zig @@ -4,6 +4,10 @@ const cli = @import("../cli.zig"); const ffmpeg = @import("../ffmpeg.zig"); pub fn generate(allocator: std.mem.Allocator, config: cli.VideoConfig) !void { + try generateWithProgress(allocator, config, false); +} + +pub fn generateWithProgress(allocator: std.mem.Allocator, config: cli.VideoConfig, show_progress: bool) !void { print("Generating video with parameters:\n", .{}); print(" Resolution: {}x{}\n", .{ config.width, config.height }); print(" Duration: {}s\n", .{config.duration}); @@ -11,7 +15,6 @@ pub fn generate(allocator: std.mem.Allocator, config: cli.VideoConfig) !void { print(" Bitrate: {s}\n", .{config.bitrate}); print(" Format: {s}\n", .{config.format}); print(" Codec: {s}\n", .{config.codec}); - print(" Output: {s}\n", .{config.output}); // Create input filter with countdown timer - black background with large white numbers @@ -41,6 +44,12 @@ pub fn generate(allocator: std.mem.Allocator, config: cli.VideoConfig) !void { }; // Execute FFmpeg command + if (show_progress) { + print("šŸŽ¬ Running FFmpeg encoder...\n", .{}); + print("ā³ Please wait, encoding {d}s video...\n", .{config.duration}); + } + + // Run FFmpeg const result = std.process.Child.run(.{ .allocator = allocator, .argv = &cmd_args, diff --git a/src/interactive.zig b/src/interactive.zig new file mode 100644 index 0000000..5c64438 --- /dev/null +++ b/src/interactive.zig @@ -0,0 +1,420 @@ +const std = @import("std"); +const print = std.debug.print; +const cli = @import("cli.zig"); +const video_gen = @import("generators/video.zig"); +const audio_gen = @import("generators/audio.zig"); + +const InteractiveConfig = union(cli.MediaType) { + video: cli.VideoConfig, + audio: cli.AudioConfig, +}; + +const Action = enum { + generate, + edit, + cancel, +}; + +pub fn run(allocator: std.mem.Allocator) !void { + print("\nšŸŽ¬ Media Generator - Interactive Mode\n", .{}); + print("=====================================\n\n", .{}); + + var config = try collectUserInput(allocator); + defer freeConfig(allocator, &config); + + // Show summary and allow editing + while (true) { + try showSummary(&config); + + const action = try askAction(); + switch (action) { + .generate => { + try generateMediaWithProgress(allocator, &config); + break; + }, + .edit => { + try editConfig(allocator, &config); + }, + .cancel => { + print("Generation cancelled.\n", .{}); + break; + }, + } + } +} + +fn collectUserInput(allocator: std.mem.Allocator) !InteractiveConfig { + // Step 1: Choose media type + const media_type = try askMediaType(); + + switch (media_type) { + .video => { + var config = cli.VideoConfig{}; + + // Collect video parameters + config.width = try askNumber(u32, "Video width", config.width); + config.height = try askNumber(u32, "Video height", config.height); + config.duration = try askNumber(u32, "Duration (seconds)", config.duration); + config.fps = try askNumber(u32, "Frames per second", config.fps); + config.bitrate = try askString(allocator, "Video bitrate", config.bitrate); + config.format = try askChoice("Video format", &[_][]const u8{ "mp4", "avi", "mov", "mkv" }, config.format); + config.codec = try askChoice("Video codec", &[_][]const u8{ "libx264", "libx265", "libvpx-vp9" }, config.codec); + config.output = try askString(allocator, "Output filename", config.output); + + return InteractiveConfig{ .video = config }; + }, + .audio => { + var config = cli.AudioConfig{}; + + // Collect audio parameters + config.duration = try askNumber(u32, "Duration (seconds)", config.duration); + config.sample_rate = try askNumber(u32, "Sample rate (Hz)", config.sample_rate); + config.frequency = try askNumber(u32, "Sine wave frequency (Hz)", config.frequency); + config.bitrate = try askString(allocator, "Audio bitrate", config.bitrate); + config.format = try askChoice("Audio format", &[_][]const u8{ "mp3", "wav", "aac", "flac" }, config.format); + config.codec = try askChoice("Audio codec", &[_][]const u8{ "libmp3lame", "pcm_s16le", "aac" }, config.codec); + config.output = try askString(allocator, "Output filename", config.output); + + return InteractiveConfig{ .audio = config }; + }, + } +} + +fn askMediaType() !cli.MediaType { + while (true) { + print("What would you like to generate?\n", .{}); + print(" 1) Video (default)\n", .{}); + print(" 2) Audio\n", .{}); + print("Choice [1]: ", .{}); + + const input = try readUserInput(std.heap.page_allocator); + defer std.heap.page_allocator.free(input); + + const trimmed = std.mem.trim(u8, input, " \t\n\r"); + + if (trimmed.len == 0 or std.mem.eql(u8, trimmed, "1")) { + return .video; + } else if (std.mem.eql(u8, trimmed, "2")) { + return .audio; + } else { + print("Invalid choice. Please enter 1 or 2.\n\n", .{}); + } + } +} + +fn askNumber(comptime T: type, prompt: []const u8, default_value: T) !T { + while (true) { + print("{s} [{d}]: ", .{ prompt, default_value }); + + const input = try readUserInput(std.heap.page_allocator); + defer std.heap.page_allocator.free(input); + + const trimmed = std.mem.trim(u8, input, " \t\n\r"); + + if (trimmed.len == 0) { + return default_value; + } + + if (std.fmt.parseInt(T, trimmed, 10)) |value| { + return value; + } else |_| { + print("Invalid number. Please try again.\n", .{}); + } + } +} + +fn askString(allocator: std.mem.Allocator, prompt: []const u8, default_value: []const u8) ![]const u8 { + print("{s} [{s}]: ", .{ prompt, default_value }); + + const input = try readUserInput(std.heap.page_allocator); + defer std.heap.page_allocator.free(input); + + const trimmed = std.mem.trim(u8, input, " \t\n\r"); + + if (trimmed.len == 0) { + return try allocator.dupe(u8, default_value); + } else { + return try allocator.dupe(u8, trimmed); + } +} + +fn askChoice(prompt: []const u8, choices: []const []const u8, default_value: []const u8) ![]const u8 { + while (true) { + print("{s}:\n", .{prompt}); + for (choices, 0..) |choice, i| { + const marker = if (std.mem.eql(u8, choice, default_value)) " (default)" else ""; + print(" {d}) {s}{s}\n", .{ i + 1, choice, marker }); + } + print("Choice: ", .{}); + + const input = try readUserInput(std.heap.page_allocator); + defer std.heap.page_allocator.free(input); + + const trimmed = std.mem.trim(u8, input, " \t\n\r"); + + if (trimmed.len == 0) { + return default_value; + } + + // Try to parse as number + if (std.fmt.parseInt(usize, trimmed, 10)) |choice_num| { + if (choice_num >= 1 and choice_num <= choices.len) { + return choices[choice_num - 1]; + } + } else |_| { + // Try to match by name + for (choices) |choice| { + if (std.mem.eql(u8, trimmed, choice)) { + return choice; + } + } + } + + print("Invalid choice. Please try again.\n\n", .{}); + } +} + +fn askAction() !Action { + while (true) { + print("What would you like to do?\n", .{}); + print(" 1) Generate file (default)\n", .{}); + print(" 2) Edit parameters\n", .{}); + print(" 3) Cancel\n", .{}); + print("Choice [1]: ", .{}); + + const input = try readUserInput(std.heap.page_allocator); + defer std.heap.page_allocator.free(input); + + const trimmed = std.mem.trim(u8, input, " \t\n\r"); + + if (trimmed.len == 0 or std.mem.eql(u8, trimmed, "1")) { + return .generate; + } else if (std.mem.eql(u8, trimmed, "2")) { + return .edit; + } else if (std.mem.eql(u8, trimmed, "3")) { + return .cancel; + } else { + print("Invalid choice. Please enter 1, 2, or 3.\n\n", .{}); + } + } +} + +fn askConfirmation(prompt: []const u8) !bool { + while (true) { + print("{s} [Y/n]: ", .{prompt}); + + const input = try readUserInput(std.heap.page_allocator); + defer std.heap.page_allocator.free(input); + + const trimmed = std.mem.trim(u8, input, " \t\n\r"); + + if (trimmed.len == 0 or + std.mem.eql(u8, trimmed, "y") or + std.mem.eql(u8, trimmed, "Y") or + std.mem.eql(u8, trimmed, "yes") or + std.mem.eql(u8, trimmed, "Yes")) + { + return true; + } else if (std.mem.eql(u8, trimmed, "n") or + std.mem.eql(u8, trimmed, "N") or + std.mem.eql(u8, trimmed, "no") or + std.mem.eql(u8, trimmed, "No")) + { + return false; + } else { + print("Please enter 'y' for yes or 'n' for no.\n", .{}); + } + } +} + +fn showSummary(config: *const InteractiveConfig) !void { + print("\nšŸ“‹ Generation Summary\n", .{}); + print("====================\n", .{}); + + switch (config.*) { + .video => |video_config| { + print("Type: Video\n", .{}); + print("Resolution: {d}x{d}\n", .{ video_config.width, video_config.height }); + print("Duration: {d} seconds\n", .{video_config.duration}); + print("FPS: {d}\n", .{video_config.fps}); + print("Bitrate: {s}\n", .{video_config.bitrate}); + print("Format: {s}\n", .{video_config.format}); + print("Codec: {s}\n", .{video_config.codec}); + print("Output: {s}\n", .{video_config.output}); + }, + .audio => |audio_config| { + print("Type: Audio\n", .{}); + print("Duration: {d} seconds\n", .{audio_config.duration}); + print("Sample Rate: {d} Hz\n", .{audio_config.sample_rate}); + print("Frequency: {d} Hz\n", .{audio_config.frequency}); + print("Bitrate: {s}\n", .{audio_config.bitrate}); + print("Format: {s}\n", .{audio_config.format}); + print("Codec: {s}\n", .{audio_config.codec}); + print("Output: {s}\n", .{audio_config.output}); + }, + } + print("\n", .{}); +} + +fn editConfig(allocator: std.mem.Allocator, config: *InteractiveConfig) !void { + print("\nšŸ”§ Edit Parameters\n", .{}); + print("==================\n", .{}); + + switch (config.*) { + .video => |*video_config| { + print("Which parameter would you like to edit?\n", .{}); + print(" 1) Width ({d})\n", .{video_config.width}); + print(" 2) Height ({d})\n", .{video_config.height}); + print(" 3) Duration ({d}s)\n", .{video_config.duration}); + print(" 4) FPS ({d})\n", .{video_config.fps}); + print(" 5) Bitrate ({s})\n", .{video_config.bitrate}); + print(" 6) Format ({s})\n", .{video_config.format}); + print(" 7) Codec ({s})\n", .{video_config.codec}); + print(" 8) Output filename ({s})\n", .{video_config.output}); + print("Choice: ", .{}); + + const input = try readUserInput(std.heap.page_allocator); + defer std.heap.page_allocator.free(input); + + const trimmed = std.mem.trim(u8, input, " \t\n\r"); + + if (std.mem.eql(u8, trimmed, "1")) { + video_config.width = try askNumber(u32, "Video width", video_config.width); + } else if (std.mem.eql(u8, trimmed, "2")) { + video_config.height = try askNumber(u32, "Video height", video_config.height); + } else if (std.mem.eql(u8, trimmed, "3")) { + video_config.duration = try askNumber(u32, "Duration (seconds)", video_config.duration); + } else if (std.mem.eql(u8, trimmed, "4")) { + video_config.fps = try askNumber(u32, "Frames per second", video_config.fps); + } else if (std.mem.eql(u8, trimmed, "5")) { + allocator.free(video_config.bitrate); + video_config.bitrate = try askString(allocator, "Video bitrate", "1000k"); + } else if (std.mem.eql(u8, trimmed, "6")) { + video_config.format = try askChoice("Video format", &[_][]const u8{ "mp4", "avi", "mov", "mkv" }, video_config.format); + } else if (std.mem.eql(u8, trimmed, "7")) { + video_config.codec = try askChoice("Video codec", &[_][]const u8{ "libx264", "libx265", "libvpx-vp9" }, video_config.codec); + } else if (std.mem.eql(u8, trimmed, "8")) { + allocator.free(video_config.output); + video_config.output = try askString(allocator, "Output filename", "output.mp4"); + } else { + print("Invalid choice.\n", .{}); + } + }, + .audio => |*audio_config| { + print("Which parameter would you like to edit?\n", .{}); + print(" 1) Duration ({d}s)\n", .{audio_config.duration}); + print(" 2) Sample rate ({d}Hz)\n", .{audio_config.sample_rate}); + print(" 3) Frequency ({d}Hz)\n", .{audio_config.frequency}); + print(" 4) Bitrate ({s})\n", .{audio_config.bitrate}); + print(" 5) Format ({s})\n", .{audio_config.format}); + print(" 6) Codec ({s})\n", .{audio_config.codec}); + print(" 7) Output filename ({s})\n", .{audio_config.output}); + print("Choice: ", .{}); + + const input = try readUserInput(std.heap.page_allocator); + defer std.heap.page_allocator.free(input); + + const trimmed = std.mem.trim(u8, input, " \t\n\r"); + + if (std.mem.eql(u8, trimmed, "1")) { + audio_config.duration = try askNumber(u32, "Duration (seconds)", audio_config.duration); + } else if (std.mem.eql(u8, trimmed, "2")) { + audio_config.sample_rate = try askNumber(u32, "Sample rate (Hz)", audio_config.sample_rate); + } else if (std.mem.eql(u8, trimmed, "3")) { + audio_config.frequency = try askNumber(u32, "Sine wave frequency (Hz)", audio_config.frequency); + } else if (std.mem.eql(u8, trimmed, "4")) { + allocator.free(audio_config.bitrate); + audio_config.bitrate = try askString(allocator, "Audio bitrate", "128k"); + } else if (std.mem.eql(u8, trimmed, "5")) { + audio_config.format = try askChoice("Audio format", &[_][]const u8{ "mp3", "wav", "aac", "flac" }, audio_config.format); + } else if (std.mem.eql(u8, trimmed, "6")) { + audio_config.codec = try askChoice("Audio codec", &[_][]const u8{ "libmp3lame", "pcm_s16le", "aac" }, audio_config.codec); + } else if (std.mem.eql(u8, trimmed, "7")) { + allocator.free(audio_config.output); + audio_config.output = try askString(allocator, "Output filename", "output.mp3"); + } else { + print("Invalid choice.\n", .{}); + } + }, + } + print("\n", .{}); +} + +fn generateMediaWithProgress(allocator: std.mem.Allocator, config: *const InteractiveConfig) !void { + print("šŸš€ Generating media file...\n", .{}); + + const duration = switch (config.*) { + .video => |video_config| video_config.duration, + .audio => |audio_config| audio_config.duration, + }; + + print("\nā±ļø Estimated time: ~{d} seconds\n", .{duration}); + print("šŸ”„ Processing", .{}); + + // Show simple spinner during actual generation + const spinner_chars = [_][]const u8{ "ā ‹", "ā ™", "ā ¹", "ā ø", "ā ¼", "ā “", "ā ¦", "ā §", "ā ‡", "ā " }; + var spinner_index: usize = 0; + + // Start a simple animation before generation + var pre_steps: u32 = 0; + while (pre_steps < 10) : (pre_steps += 1) { + print("\ršŸ”„ Processing {s} Starting...", .{spinner_chars[spinner_index]}); + spinner_index = (spinner_index + 1) % spinner_chars.len; + + var delay: u32 = 0; + while (delay < 3000000) : (delay += 1) { + // Short delay + } + } + + print("\ršŸ”„ Processing... Running FFmpeg\n", .{}); + + // Now do the actual generation with progress + switch (config.*) { + .video => |video_config| { + try video_gen.generateWithProgress(allocator, video_config, true); + }, + .audio => |audio_config| { + try audio_gen.generateWithProgress(allocator, audio_config, true); + }, + } +} + +fn generateMedia(allocator: std.mem.Allocator, config: *const InteractiveConfig) !void { + print("šŸš€ Generating media file...\n\n", .{}); + + switch (config.*) { + .video => |video_config| { + try video_gen.generate(allocator, video_config); + }, + .audio => |audio_config| { + try audio_gen.generate(allocator, audio_config); + }, + } +} + +fn freeConfig(allocator: std.mem.Allocator, config: *const InteractiveConfig) void { + switch (config.*) { + .video => |video_config| { + allocator.free(video_config.bitrate); + allocator.free(video_config.output); + }, + .audio => |audio_config| { + allocator.free(audio_config.bitrate); + allocator.free(audio_config.output); + }, + } +} + +fn readUserInput(allocator: std.mem.Allocator) ![]u8 { + var buffer: [1024]u8 = undefined; + const bytes_read = try std.posix.read(std.posix.STDIN_FILENO, &buffer); + + if (bytes_read > 0) { + // Remove trailing newline if present + const end = if (buffer[bytes_read - 1] == '\n') bytes_read - 1 else bytes_read; + return try allocator.dupe(u8, buffer[0..end]); + } else { + return try allocator.dupe(u8, ""); + } +}