diff --git a/Cargo.lock b/Cargo.lock index 5c51c80..d75eac9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -525,7 +525,7 @@ checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" [[package]] name = "t-rec" -version = "0.1.2" +version = "0.2.0" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index e50f229..5ee2890 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "t-rec" -version = "0.1.2" +version = "0.2.0" authors = ["Sven Assmann "] edition = "2018" license = "GPL-3.0-only" diff --git a/README.md b/README.md index 78031b5..6e9f5e6 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Blazingly fast terminal recorder that generates animated gif images for the web - Screenshotting your terminal with 4 frames per second (every 250ms) - Generates high quality small sized animated gif images +- **Build-In idle frames detection and optimization** (for super fluid presentations) - Runs (only) on MacOS - Uses native efficient APIs - Runs without any cloud service and entirely offline @@ -48,7 +49,37 @@ or with specifying a different program to launch ❯ t-rec /bin/sh ``` -### Hidden Gems +### Full Options + +```sh +❯ t-rec --help +t-rec 0.2.0 +Sven Assmann +Blazingly fast terminal recorder that generates animated gif images for the web written in rust. + +USAGE: + t-rec [FLAGS] [shell or program to launch] + +FLAGS: + -h, --help Prints help information + -l, --ls-win If you want to see a list of windows available for recording by their id, you can set env var + 'WINDOWID' to record this specific window only. + -n, --natural If you want a very natural typing experience and disable the idle detection and sampling + optimization. + -V, --version Prints version information + +ARGS: + If you want to start a different program than $SHELL you can pass it here. For + example '/bin/sh' +``` + +### Disable idle detection & optimization + +If you are not happy with the idle detection and optimization, you can disable it with the `-n` or `--natural` parameter. +By doing so, you would get the very natural timeline of typing and recording as you do it. +In this case there will be no optimizations performed. + +## Hidden Gems You can record not only the terminal but also every other window. There 2 ways to do so: @@ -58,9 +89,9 @@ You can record not only the terminal but also every other window. There 2 ways t ```sh ❯ TERM_PROGRAM="google chrome" t-rec -tmp path: "/var/folders/m8/084p1v0x4770rpwpkrgl5b6h0000gn/T/trec-74728.rUxBx3ohGiQ2" +Frame cache dir: "/var/folders/m8/084p1v0x4770rpwpkrgl5b6h0000gn/T/trec-74728.rUxBx3ohGiQ2" Press Ctrl+D to end recording -[src/window_id.rs:122] window_owner = "Google Chrome 2" +Recording Window: "Google Chrome 2" ``` this is how it looks then: @@ -79,7 +110,7 @@ Code | 27600 # set the WINDOWID variable and run t-rec ❯ WINDOWID=27600 t-rec -tmp path: "/var/folders/m8/084p1v0x4770rpwpkrgl5b6h0000gn/T/trec-77862.BMYiHNRWqv9Y" +Frame cache dir: "/var/folders/m8/084p1v0x4770rpwpkrgl5b6h0000gn/T/trec-77862.BMYiHNRWqv9Y" Press Ctrl+D to end recording ``` diff --git a/docs/demo.gif b/docs/demo.gif index ffbdf46..631ea0a 100644 Binary files a/docs/demo.gif and b/docs/demo.gif differ diff --git a/src/any/mod.rs b/src/any/mod.rs index 7c40958..cb6bd86 100644 --- a/src/any/mod.rs +++ b/src/any/mod.rs @@ -1,4 +1,4 @@ -use tempfile::TempDir; +use crate::ImageOnHeap; pub fn get_window_id_for(_terminal: String) -> Option { unimplemented!("there is only an impl for MacOS") @@ -8,12 +8,7 @@ pub fn ls_win() { unimplemented!("there is only an impl for MacOS") } -pub fn screenshot_and_save( - _win_id: u32, - _time_code: u128, - _tempdir: &TempDir, - _file_name_for: fn(&u128, &str) -> String, -) -> anyhow::Result<()> { +pub fn capture_window_screenshot(_win_id: u32) -> anyhow::Result { unimplemented!("there is only an impl for MacOS") } diff --git a/src/cli.rs b/src/cli.rs index 742d007..973d3f6 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -6,6 +6,15 @@ pub fn launch<'a>() -> ArgMatches<'a> { .author(crate_authors!()) .about(crate_description!()) .setting(AppSettings::AllowMissingPositional) + .arg( + Arg::with_name("natural-mode") + .value_name("natural") + .takes_value(false) + .required(false) + .short("n") + .long("natural") + .help("If you want a very natural typing experience and disable the idle detection and sampling optimization.") + ) .arg( Arg::with_name("list-windows") .value_name("list all visible windows with name and id") diff --git a/src/macos/mod.rs b/src/macos/mod.rs index 5e7758d..a43f9cc 100644 --- a/src/macos/mod.rs +++ b/src/macos/mod.rs @@ -2,5 +2,5 @@ mod core_foundation_sys_patches; mod screenshot; mod window_id; -pub use screenshot::screenshot_and_save; +pub use screenshot::capture_window_screenshot; pub use window_id::{get_window_id_for, ls_win}; diff --git a/src/macos/screenshot.rs b/src/macos/screenshot.rs index 403d01f..8416fa5 100644 --- a/src/macos/screenshot.rs +++ b/src/macos/screenshot.rs @@ -1,19 +1,11 @@ +use crate::ImageOnHeap; use anyhow::{Context, Result}; use core_graphics::display::*; use core_graphics::image::CGImageRef; use image::flat::SampleLayout; -use image::{save_buffer, ColorType, FlatSamples}; -use tempfile::TempDir; +use image::{ColorType, FlatSamples}; -/// -/// grabs a screenshot by window id and -/// saves it as a tga file -pub fn screenshot_and_save( - win_id: u32, - time_code: u128, - tempdir: &TempDir, - file_name_for: fn(&u128, &str) -> String, -) -> Result<()> { +pub fn capture_window_screenshot(win_id: u32) -> Result { let (w, h, channels, raw_data) = { let image = unsafe { CGDisplay::screenshot( @@ -51,12 +43,5 @@ pub fn screenshot_and_save( color_hint: Some(color), }; - save_buffer( - tempdir.path().join(file_name_for(&time_code, "tga")), - &buffer.samples, - w, - h, - color, - ) - .context("Cannot save frame") + Ok(ImageOnHeap::new(buffer)) } diff --git a/src/main.rs b/src/main.rs index f168b11..8380b60 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,20 +1,23 @@ #[cfg(target_os = "macos")] mod macos; #[cfg(target_os = "macos")] -use macos::{get_window_id_for, ls_win, screenshot_and_save}; +use macos::*; #[cfg(not(target_os = "macos"))] mod any; #[cfg(not(target_os = "macos"))] -use any::{get_window_id_for, ls_win, screenshot_and_save}; +use any::*; mod cli; use crate::cli::launch; +use crate::macos::capture_window_screenshot; use anyhow::Context; use anyhow::Result; +use image::{save_buffer, FlatSamples}; use std::borrow::Borrow; use std::ffi::OsStr; +use std::ops::{Add, Sub}; use std::process::{Command, ExitStatus, Output}; use std::sync::mpsc::Receiver; use std::sync::{mpsc, Arc, Mutex}; @@ -22,6 +25,8 @@ use std::time::{Duration, Instant}; use std::{env, thread}; use tempfile::TempDir; +pub type ImageOnHeap = Box>>; + #[cfg(target_os = "linux")] fn main() -> Result<(), std::io::Error> { unimplemented!("We're super sorry, right now t-rec is only supporting MacOS.\nIf you'd like to contribute checkout:\n\nhttps://github.com/sassman/t-rec-rs/issues/1\n") @@ -48,6 +53,8 @@ fn main() -> Result<()> { } }; + let force_natural = args.is_present("natural-mode"); + check_for_imagemagick()?; // the nice thing is the cleanup on drop @@ -59,7 +66,10 @@ fn main() -> Result<()> { let photograph = { let tempdir = tempdir.clone(); let time_codes = time_codes.clone(); - thread::spawn(move || -> Result<()> { capture_thread(&rx, time_codes, tempdir) }) + let force_natural = force_natural; + thread::spawn(move || -> Result<()> { + capture_thread(&rx, time_codes, tempdir, force_natural) + }) }; let interact = thread::spawn(move || -> Result<()> { sub_shell_thread(&program).map(|_| ()) }); @@ -102,23 +112,70 @@ fn capture_thread( rx: &Receiver<()>, time_codes: Arc>>, tempdir: Arc>, + force_natural: bool, ) -> Result<()> { let win_id = current_win_id()?; let duration = Duration::from_millis(250); let start = Instant::now(); + let mut idle_duration = Duration::from_millis(0); + let mut last_frame: Option = None; + let mut identical_frames = 0; + let mut last_now = Instant::now(); loop { // blocks for a timeout if rx.recv_timeout(duration).is_ok() { break; } - let tc = Instant::now().saturating_duration_since(start).as_millis(); - time_codes.lock().unwrap().push(tc); - screenshot_and_save(win_id, tc, tempdir.lock().unwrap().borrow(), file_name_for)? + let now = Instant::now(); + let effective_now = now.sub(idle_duration); + let tc = effective_now.saturating_duration_since(start).as_millis(); + let image = capture_window_screenshot(win_id)?; + if !force_natural { + if last_frame.is_some() + && image + .samples + .as_slice() + .eq(last_frame.as_ref().unwrap().samples.as_slice()) + { + identical_frames += 1; + } else { + identical_frames = 0; + } + } + + if identical_frames > 0 { + // let's track now the duration as idle + idle_duration = idle_duration.add(now.duration_since(last_now)); + } else { + save_frame(&image, tc, tempdir.lock().unwrap().borrow(), file_name_for)?; + time_codes.lock().unwrap().push(tc); + last_frame = Some(image); + identical_frames = 0; + } + last_now = now; } Ok(()) } +/// +/// saves a frame as a tga file +pub fn save_frame( + image: &ImageOnHeap, + time_code: u128, + tempdir: &TempDir, + file_name_for: fn(&u128, &str) -> String, +) -> Result<()> { + save_buffer( + tempdir.path().join(file_name_for(&time_code, "tga")), + &image.samples, + image.layout.width, + image.layout.height, + image.color_hint.unwrap(), + ) + .context("Cannot save frame") +} + /// /// starts the main program and keeps interacting with the user /// blocks until termination