1
1
mirror of https://github.com/wez/wezterm.git synced 2024-11-22 04:56:12 +03:00

fixup images with the multiplexer

* Translate from File to EncodedFile as needed
* Adopt blob leases in the mux server
* Fix an issue where the first image sent by the mux server would
  be replaced on the client by its background image, if configured.
  Removed the ImageData::id field to resolve this: you should use
  the hash field instead to identify and disambiguate images.
  Bumped the termwiz API version because this is conceptually
  a breaking change to the API

refs: https://github.com/wez/wezterm/issues/3343
This commit is contained in:
Wez Furlong 2023-03-23 21:40:01 -07:00
parent c38d160918
commit bc7acc18e0
No known key found for this signature in database
GPG Key ID: 7A7F66A31EC9B387
12 changed files with 80 additions and 51 deletions

3
Cargo.lock generated
View File

@ -5045,7 +5045,7 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76"
[[package]] [[package]]
name = "termwiz" name = "termwiz"
version = "0.21.0" version = "0.22.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64 0.21.0", "base64 0.21.0",
@ -6106,6 +6106,7 @@ dependencies = [
"portable-pty", "portable-pty",
"promise", "promise",
"umask", "umask",
"wezterm-blob-leases",
"wezterm-gui-subcommands", "wezterm-gui-subcommands",
"wezterm-mux-server-impl", "wezterm-mux-server-impl",
"wezterm-term", "wezterm-term",

View File

@ -417,7 +417,7 @@ macro_rules! pdu {
/// The overall version of the codec. /// The overall version of the codec.
/// This must be bumped when backwards incompatible changes /// This must be bumped when backwards incompatible changes
/// are made to the types and protocol. /// are made to the types and protocol.
pub const CODEC_VERSION: usize = 32; pub const CODEC_VERSION: usize = 33;
// Defines the Pdu enum. // Defines the Pdu enum.
// Each struct has an explicit identifying number. // Each struct has an explicit identifying number.

View File

@ -52,6 +52,7 @@ As features stabilize some brief notes about them will accumulate here.
* Lingering openconsole.exe processes on Windows. Thanks to @mbikovitsky! #3339 * Lingering openconsole.exe processes on Windows. Thanks to @mbikovitsky! #3339
* macos: command line parameters beyond the first were treated as terminal * macos: command line parameters beyond the first were treated as terminal
command scripts and opened spurious windows. #3340 command scripts and opened spurious windows. #3340
* imgcat broken with multiplexer protocol #3343
### 20230320-124340-559cb7b0 ### 20230320-124340-559cb7b0

View File

@ -9,4 +9,4 @@ license = "MIT"
documentation = "https://docs.rs/tabout" documentation = "https://docs.rs/tabout"
[dependencies] [dependencies]
termwiz = { path = "../termwiz", version="0.21"} termwiz = { path = "../termwiz", version="0.22"}

View File

@ -40,6 +40,6 @@ env_logger = "0.10"
k9 = "0.11.0" k9 = "0.11.0"
[dependencies.termwiz] [dependencies.termwiz]
version = "0.21" version = "0.22"
path = "../termwiz" path = "../termwiz"
features = ["use_image"] features = ["use_image"]

View File

@ -1,7 +1,7 @@
[package] [package]
authors = ["Wez Furlong"] authors = ["Wez Furlong"]
name = "termwiz" name = "termwiz"
version = "0.21.0" version = "0.22.0"
edition = "2018" edition = "2018"
repository = "https://github.com/wez/wezterm" repository = "https://github.com/wez/wezterm"
description = "Terminal Wizardry for Unix and Windows" description = "Terminal Wizardry for Unix and Windows"

View File

@ -471,20 +471,35 @@ impl ImageDataType {
} }
} }
static IMAGE_ID: ::std::sync::atomic::AtomicUsize = ::std::sync::atomic::AtomicUsize::new(0);
#[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "use_serde", derive(Serialize, Deserialize))]
#[derive(Debug)]
pub struct ImageData { pub struct ImageData {
id: usize,
data: Mutex<ImageDataType>, data: Mutex<ImageDataType>,
hash: [u8; 32], hash: [u8; 32],
} }
struct HexSlice<'a>(&'a [u8]);
impl<'a> std::fmt::Display for HexSlice<'a> {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
for byte in self.0 {
write!(fmt, "{byte:x}")?;
}
Ok(())
}
}
impl std::fmt::Debug for ImageData {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
fmt.debug_struct("ImageData")
.field("data", &self.data)
.field("hash", &format_args!("{}", HexSlice(&self.hash)))
.finish()
}
}
impl Eq for ImageData {} impl Eq for ImageData {}
impl PartialEq for ImageData { impl PartialEq for ImageData {
fn eq(&self, rhs: &Self) -> bool { fn eq(&self, rhs: &Self) -> bool {
self.id == rhs.id self.hash == rhs.hash
} }
} }
@ -496,19 +511,15 @@ impl ImageData {
} }
fn with_data_and_hash(data: ImageDataType, hash: [u8; 32]) -> Self { fn with_data_and_hash(data: ImageDataType, hash: [u8; 32]) -> Self {
let id = IMAGE_ID.fetch_add(1, ::std::sync::atomic::Ordering::Relaxed);
Self { Self {
id,
data: Mutex::new(data), data: Mutex::new(data),
hash, hash,
} }
} }
pub fn with_data(data: ImageDataType) -> Self { pub fn with_data(data: ImageDataType) -> Self {
let id = IMAGE_ID.fetch_add(1, ::std::sync::atomic::Ordering::Relaxed);
let hash = data.compute_hash(); let hash = data.compute_hash();
Self { Self {
id,
data: Mutex::new(data), data: Mutex::new(data),
hash, hash,
} }
@ -528,10 +539,6 @@ impl ImageData {
self.data.lock().unwrap() self.data.lock().unwrap()
} }
pub fn id(&self) -> usize {
self.id
}
pub fn hash(&self) -> [u8; 32] { pub fn hash(&self) -> [u8; 32] {
self.hash self.hash
} }

View File

@ -666,16 +666,23 @@ pub(crate) async fn hydrate_lines(
} }
for (_, request) in requests { for (_, request) in requests {
log::trace!("requesting {:?}", request); match client.client.get_image_cell(request).await {
if let Ok(GetImageCellResponse { Ok(GetImageCellResponse {
data: Some(data), .. data: Some(data), ..
}) = client.client.get_image_cell(request).await }) => {
{ IMAGE_LRU
IMAGE_LRU .lock()
.lock() .unwrap()
.unwrap() .put(data.hash(), Arc::clone(&data));
.put(data.hash(), Arc::clone(&data)); data_by_hash.insert(data.hash(), data);
data_by_hash.insert(data.hash(), data); }
Ok(GetImageCellResponse { data: None, .. }) => {
log::error!("no image data!");
}
Err(err) => {
log::error!("failed to retrieve image {err:#}");
}
} }
} }

View File

@ -9,7 +9,7 @@ use config::{AllowSquareGlyphOverflow, TextStyle};
use euclid::num::Zero; use euclid::num::Zero;
use image::io::Limits; use image::io::Limits;
use image::{AnimationDecoder, Frame, Frames, ImageDecoder, ImageFormat, ImageResult}; use image::{AnimationDecoder, Frame, Frames, ImageDecoder, ImageFormat, ImageResult};
use lfucache::LfuCacheU64; use lfucache::LfuCache;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use ordered_float::NotNan; use ordered_float::NotNan;
use std::cell::RefCell; use std::cell::RefCell;
@ -441,27 +441,33 @@ impl DecodedImage {
} }
} }
fn start_frame_decoder(lease: BlobLease, image_data: &Arc<ImageData>) -> Self {
match FrameDecoder::start(lease.clone()) {
Ok(rx) => Self {
frame_start: RefCell::new(Instant::now()),
current_frame: RefCell::new(0),
image: Arc::clone(image_data),
frames: RefCell::new(Some(FrameState::new(rx))),
},
Err(err) => {
log::error!("failed to start FrameDecoder: {err:#}");
Self::placeholder()
}
}
}
fn load(image_data: &Arc<ImageData>) -> Self { fn load(image_data: &Arc<ImageData>) -> Self {
match &*image_data.data() { match &*image_data.data() {
ImageDataType::EncodedLease(lease) => match FrameDecoder::start(lease.clone()) { ImageDataType::EncodedLease(lease) => {
Ok(rx) => Self { Self::start_frame_decoder(lease.clone(), image_data)
frame_start: RefCell::new(Instant::now()), }
current_frame: RefCell::new(0), ImageDataType::EncodedFile(data) => match BlobManager::store(&data) {
image: Arc::clone(image_data), Ok(lease) => Self::start_frame_decoder(lease, image_data),
frames: RefCell::new(Some(FrameState::new(rx))),
},
Err(err) => { Err(err) => {
log::error!("failed to start FrameDecoder: {err:#}"); log::error!("Unable to move file data to blob manager: {err:#}");
Self::placeholder() Self::placeholder()
} }
}, },
ImageDataType::EncodedFile(_) => {
log::error!("Unexpected EncodedFile; should have File here");
// The swap_out call happens in the terminal layer
// when normalizing/caching the data just prior to
// applying it to the terminal model
Self::placeholder()
}
ImageDataType::AnimRgba8 { durations, .. } => { ImageDataType::AnimRgba8 { durations, .. } => {
let current_frame = if durations.len() > 1 && durations[0].as_millis() == 0 { let current_frame = if durations.len() > 1 && durations[0].as_millis() == 0 {
// Skip possible 0-duration root frame // Skip possible 0-duration root frame
@ -493,7 +499,7 @@ pub struct GlyphCache {
glyph_cache: HashMap<GlyphKey, Rc<CachedGlyph>>, glyph_cache: HashMap<GlyphKey, Rc<CachedGlyph>>,
pub atlas: Atlas, pub atlas: Atlas,
pub fonts: Rc<FontConfiguration>, pub fonts: Rc<FontConfiguration>,
pub image_cache: LfuCacheU64<DecodedImage>, pub image_cache: LfuCache<[u8; 32], DecodedImage>,
frame_cache: HashMap<[u8; 32], Sprite>, frame_cache: HashMap<[u8; 32], Sprite>,
line_glyphs: HashMap<LineKey, Sprite>, line_glyphs: HashMap<LineKey, Sprite>,
pub block_glyphs: HashMap<SizedBlockKey, Sprite>, pub block_glyphs: HashMap<SizedBlockKey, Sprite>,
@ -510,7 +516,7 @@ impl GlyphCache {
Ok(Self { Ok(Self {
fonts: Rc::clone(fonts), fonts: Rc::clone(fonts),
glyph_cache: HashMap::new(), glyph_cache: HashMap::new(),
image_cache: LfuCacheU64::new( image_cache: LfuCache::new(
"glyph_cache.image_cache.hit.rate", "glyph_cache.image_cache.hit.rate",
"glyph_cache.image_cache.miss.rate", "glyph_cache.image_cache.miss.rate",
|config| config.glyph_cache_image_cache_size, |config| config.glyph_cache_image_cache_size,
@ -539,7 +545,7 @@ impl GlyphCache {
Ok(Self { Ok(Self {
fonts: Rc::clone(fonts), fonts: Rc::clone(fonts),
glyph_cache: HashMap::new(), glyph_cache: HashMap::new(),
image_cache: LfuCacheU64::new( image_cache: LfuCache::new(
"glyph_cache.image_cache.hit.rate", "glyph_cache.image_cache.hit.rate",
"glyph_cache.image_cache.miss.rate", "glyph_cache.image_cache.miss.rate",
|config| config.glyph_cache_image_cache_size, |config| config.glyph_cache_image_cache_size,
@ -947,9 +953,9 @@ impl GlyphCache {
image_data: &Arc<ImageData>, image_data: &Arc<ImageData>,
padding: Option<usize>, padding: Option<usize>,
) -> anyhow::Result<(Sprite, Option<Instant>)> { ) -> anyhow::Result<(Sprite, Option<Instant>)> {
let id = image_data.id() as u64; let hash = image_data.hash();
if let Some(decoded) = self.image_cache.get(&id) { if let Some(decoded) = self.image_cache.get(&hash) {
Self::cached_image_impl( Self::cached_image_impl(
&mut self.frame_cache, &mut self.frame_cache,
&mut self.atlas, &mut self.atlas,
@ -966,7 +972,7 @@ impl GlyphCache {
padding, padding,
self.min_frame_duration, self.min_frame_duration,
)?; )?;
self.image_cache.put(id, decoded); self.image_cache.put(hash, decoded);
Ok(res) Ok(res)
} }
} }

View File

@ -21,6 +21,7 @@ openssl = "0.10"
portable-pty = { path = "../pty", features = ["serde_support"]} portable-pty = { path = "../pty", features = ["serde_support"]}
promise = { path = "../promise" } promise = { path = "../promise" }
umask = { path = "../umask" } umask = { path = "../umask" }
wezterm-blob-leases = { path = "../wezterm-blob-leases", version="0.1", features=["simple_tempdir"] }
wezterm-mux-server-impl = { path = "../wezterm-mux-server-impl" } wezterm-mux-server-impl = { path = "../wezterm-mux-server-impl" }
wezterm-gui-subcommands = { path = "../wezterm-gui-subcommands" } wezterm-gui-subcommands = { path = "../wezterm-gui-subcommands" }
wezterm-term = { path = "../term" } wezterm-term = { path = "../term" }

View File

@ -60,9 +60,11 @@ struct Opt {
fn main() { fn main() {
if let Err(err) = run() { if let Err(err) = run() {
wezterm_blob_leases::clear_storage();
log::error!("{:#}", err); log::error!("{:#}", err);
std::process::exit(1); std::process::exit(1);
} }
wezterm_blob_leases::clear_storage();
} }
fn run() -> anyhow::Result<()> { fn run() -> anyhow::Result<()> {
@ -159,6 +161,10 @@ fn run() -> anyhow::Result<()> {
std::env::remove_var(name); std::env::remove_var(name);
} }
wezterm_blob_leases::register_storage(Arc::new(
wezterm_blob_leases::simple_tempdir::SimpleTempDir::new()?,
))?;
let need_builder = !opts.prog.is_empty() || opts.cwd.is_some(); let need_builder = !opts.prog.is_empty() || opts.cwd.is_some();
let cmd = if need_builder { let cmd = if need_builder {

View File

@ -49,5 +49,5 @@ env_logger = "0.10"
rstest = "0.17" rstest = "0.17"
shell-words = "1.1" shell-words = "1.1"
smol-potat = "1.1.2" smol-potat = "1.1.2"
termwiz = { version = "0.21", path = "../termwiz" } termwiz = { version = "0.22", path = "../termwiz" }
whoami = "1.1" whoami = "1.1"