Skip to content
1,265 changes: 1,224 additions & 41 deletions src/commands/capture.rs

Large diffs are not rendered by default.

120 changes: 116 additions & 4 deletions src/commands/export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,9 @@ pub fn run(args: ExportArgs) -> Result<()> {
if let Some((new_w, new_h)) =
resolve_export_resolution(&args, score.layout.width, score.layout.height)?
{
let (old_w, old_h) = (score.layout.width, score.layout.height);
rescale_layout(&mut score, new_w, new_h);
eprintln!(
"note: overriding resolution to {}x{} (capture was {}x{})",
new_w, new_h, score.layout.width, score.layout.height
);
eprintln!("{}", resolution_override_note(new_w, new_h, old_w, old_h));
}

if faithful {
Expand Down Expand Up @@ -194,6 +192,7 @@ fn rescale_layout(score: &mut Score, new_w: u32, new_h: u32) {
}
let scale_x = new_w as f64 / old_w as f64;
let scale_y = new_h as f64 / old_h as f64;
let font_scale = scale_x.min(scale_y);

score.layout.width = new_w;
score.layout.height = new_h;
Expand All @@ -203,9 +202,21 @@ fn rescale_layout(score: &mut Score, new_w: u32, new_h: u32) {
pane.y = (pane.y as f64 * scale_y).round() as u32;
pane.width = (pane.width as f64 * scale_x).round() as u32;
pane.height = (pane.height as f64 * scale_y).round() as u32;
if pane.kind == crate::model::PaneKind::Terminal {
if let Some(ref mut fs) = pane.font_size {
*fs = ((*fs as f64 * font_scale).round() as u32).max(1);
}
}
}
}

fn resolution_override_note(new_w: u32, new_h: u32, old_w: u32, old_h: u32) -> String {
format!(
"note: overriding resolution to {}x{} (capture was {}x{})",
new_w, new_h, old_w, old_h
)
}

#[cfg(test)]
mod tests {
/// The multiplier a demo is published at used to survive only in whoever ran
Expand Down Expand Up @@ -403,6 +414,107 @@ mod tests {
assert_eq!(score.layout.panes[0].height, 100);
}

#[test]
fn rescale_layout_scales_terminal_font_size_by_half() {
use crate::export::run::Recording;
let mut score: Score = toml::from_str(
r#"
[demo]
name = "t"
[layout]
width = 800
height = 480
[[layout.panes]]
id = "c"
type = "terminal"
x = 0
y = 0
width = 800
height = 480
font_size = 20
"#,
)
.unwrap();
let rec = Recording {
cols: 80,
rows: 24,
title: "t".into(),
events: vec![],
captions: vec![],
focuses: vec![],
duration: 0.0,
};
let plan_before = crate::export::raster::plan(&rec, &score);
rescale_layout(&mut score, 400, 240);
let plan_after = crate::export::raster::plan(&rec, &score);
assert_eq!(plan_after.width, plan_before.width / 2);
assert_eq!(plan_after.height, plan_before.height / 2);
}

#[test]
fn rescale_layout_non_square_picks_smaller_font_factor() {
let mut score: Score = toml::from_str(
r#"
[demo]
name = "t"
[layout]
width = 100
height = 100
[[layout.panes]]
id = "c"
type = "terminal"
x = 0
y = 0
width = 100
height = 100
font_size = 20
"#,
)
.unwrap();
rescale_layout(&mut score, 50, 200);
assert_eq!(score.layout.panes[0].font_size, Some(10));
}

#[test]
fn rescale_layout_font_size_floor_is_one() {
let mut score: Score = toml::from_str(
r#"
[demo]
name = "t"
[layout]
width = 100
height = 100
[[layout.panes]]
id = "c"
type = "terminal"
x = 0
y = 0
width = 100
height = 100
font_size = 2
"#,
)
.unwrap();
rescale_layout(&mut score, 10, 10);
assert_eq!(score.layout.panes[0].font_size, Some(1));
}

#[test]
fn rescale_layout_does_not_scale_browser_font_size() {
let mut score = test_score(100, 100);
score.layout.panes[1].font_size = Some(20);
rescale_layout(&mut score, 200, 200);
assert_eq!(score.layout.panes[1].font_size, Some(20));
}

#[test]
fn resolution_override_note_names_real_original_size() {
let note = resolution_override_note(1280, 720, 800, 480);
assert!(note.contains("capture was 800x480"));
assert!(!note.contains("capture was 1280x720"));
assert!(note.contains("overriding resolution to 1280x720"));
}

#[test]
fn resolve_export_resolution_from_resolution() {
let args = ExportArgs {
Expand Down
21 changes: 18 additions & 3 deletions src/commands/focus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,29 @@ pub fn run(args: FocusArgs) -> Result<()> {
control::find()?;
let sources = control::read_sources();

// Whether we're running inside the captured shell — if so, the capture's
// input thread opened a mute span when it saw `demo focus` typed, and we
// must close it on every exit path (success or failure).
let in_session = in_session();

let result = run_inner(args, &sources, in_session);
if result.is_err() && in_session {
// Close the mute span that the input thread opened when it saw the
// command typed, so a failure doesn't leave 90s of black.
let _ = control::send(serde_json::json!({ "cmd": "reveal_cancel" }));
}
result
}

fn run_inner(args: FocusArgs, sources: &[Source], in_session: bool) -> Result<()> {
let wizard_out = if args.sources.is_empty() {
if !std::io::stdin().is_terminal() {
return Err(Error::Export(
"demo focus needs a source (e.g. `demo focus main`), or a terminal for the wizard"
.to_string(),
));
}
Some(wizard(&sources, &args)?)
Some(wizard(sources, &args)?)
} else {
None
};
Expand Down Expand Up @@ -76,11 +91,11 @@ pub fn run(args: FocusArgs) -> Result<()> {
}

// Resolve each id to a reveal pane (terminal, or a browser source's URL).
let panes = build_panes(&chosen, split_with_main, &sources, args.theme.as_deref())?;
let panes = build_panes(&chosen, split_with_main, sources, args.theme.as_deref())?;

// In-session, mute this command's echo/wizard from now (from another terminal
// there's nothing in the captured shell to mute).
if in_session() {
if in_session {
let _ = control::send(serde_json::json!({ "cmd": "reveal_begin" }));
}

Expand Down
9 changes: 9 additions & 0 deletions src/commands/open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,15 @@ pub fn run(args: OpenArgs) -> Result<()> {
let _ = control::send(serde_json::json!({ "cmd": "reveal_begin" }));
}

let result = run_inner(args, in_session);
if result.is_err() && in_session {
// Close the mute span so a failure doesn't leave 90s of black.
let _ = control::send(serde_json::json!({ "cmd": "reveal_cancel" }));
}
result
}

fn run_inner(args: OpenArgs, in_session: bool) -> Result<()> {
let r = resolve(args, in_session)?;

if r.view {
Expand Down
1 change: 1 addition & 0 deletions src/export/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,7 @@ mod tests {
}

#[test]
#[allow(clippy::chunks_exact_to_as_chunks)]
fn png_to_rgba_crops_to_target_size() {
// Create a 4x4 RGB PNG
let mut buf = std::io::Cursor::new(Vec::new());
Expand Down
1 change: 1 addition & 0 deletions src/export/composite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ pub struct Layer<'a> {

/// Composite `layers` onto a `canvas_w`×`canvas_h` canvas filled with `bg`.
/// Layers are drawn in order (later layers on top) and clipped to the canvas.
#[allow(clippy::chunks_exact_to_as_chunks)]
pub fn composite(canvas_w: usize, canvas_h: usize, bg: [u8; 3], layers: &[Layer]) -> Vec<u8> {
let mut img = vec![0u8; canvas_w * canvas_h * 4];
for px in img.as_chunks_mut::<4>().0 {
Expand Down
2 changes: 2 additions & 0 deletions src/export/raster.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1437,6 +1437,7 @@ fps = 0
}

#[test]
#[allow(clippy::chunks_exact_to_as_chunks)]
fn blit_glyph_clips_at_top() {
let mut img_clipped = vec![0u8; 20 * 20 * 4];
let mut img_full = vec![0u8; 20 * 20 * 4];
Expand Down Expand Up @@ -1548,6 +1549,7 @@ fps = 0
// ── render_cells ───────────────────────────────────────────────

#[test]
#[allow(clippy::chunks_exact_to_as_chunks)]
fn render_cells_empty_screen() {
let rec = Recording {
cols: 4,
Expand Down
2 changes: 1 addition & 1 deletion src/export/recording.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,7 @@ fn browser_pane(

/// The `t_ms` at which the user started typing the final `demo stop` line, if the
/// capture ended that way — so its echo (and the "stopping" message) is dropped.
fn stop_cutoff_ms(raw: &RawMacro) -> Option<u64> {
pub fn stop_cutoff_ms(raw: &RawMacro) -> Option<u64> {
let mut line = String::new();
let mut line_start: Option<u64> = None;
let mut cutoff: Option<u64> = None;
Expand Down
Loading