From 0eaf17871a79278d56bac0ba83cdae85f8e5ce03 Mon Sep 17 00:00:00 2001 From: Manos Pitsidianakis Date: Sun, 8 Dec 2019 10:57:36 +0200 Subject: [PATCH] melib: add set_tags command in BackendOp --- melib/src/backends.rs | 14 ++++ melib/src/backends/imap.rs | 14 +++- melib/src/backends/imap/operations.rs | 54 +++++++++++-- melib/src/backends/imap/protocol_parser.rs | 19 ++--- melib/src/backends/imap/watch.rs | 79 +++++++++++++++++-- melib/src/backends/maildir.rs | 4 + melib/src/backends/mbox.rs | 4 + melib/src/backends/notmuch.rs | 57 ++++++++++++- ui/src/cache.rs | 11 ++- ui/src/components/mail/listing.rs | 45 ++++------- .../components/mail/listing/conversations.rs | 3 + ui/src/terminal/cells.rs | 10 ++- 12 files changed, 255 insertions(+), 59 deletions(-) diff --git a/melib/src/backends.rs b/melib/src/backends.rs index a72a3e35c..c571fbca3 100644 --- a/melib/src/backends.rs +++ b/melib/src/backends.rs @@ -18,6 +18,16 @@ * You should have received a copy of the GNU General Public License * along with meli. If not, see . */ + +#[macro_export] +macro_rules! tag_hash { + ($tag:ident) => {{ + let mut hasher = DefaultHasher::new(); + hasher.write($tag.as_bytes()); + hasher.finish() + }}; +} + #[cfg(feature = "imap_backend")] pub mod imap; #[cfg(feature = "maildir_backend")] @@ -288,6 +298,7 @@ pub trait BackendOp: ::std::fmt::Debug + ::std::marker::Send { //fn copy(&self fn fetch_flags(&self) -> Flag; fn set_flag(&mut self, envelope: &mut Envelope, flag: Flag, value: bool) -> Result<()>; + fn set_tag(&mut self, envelope: &mut Envelope, tag: String, value: bool) -> Result<()>; } /// Wrapper for BackendOps that are to be set read-only. @@ -318,6 +329,9 @@ impl BackendOp for ReadOnlyOp { fn set_flag(&mut self, _envelope: &mut Envelope, _flag: Flag, _value: bool) -> Result<()> { Err(MeliError::new("read-only set.")) } + fn set_tag(&mut self, _envelope: &mut Envelope, _tag: String, _value: bool) -> Result<()> { + Err(MeliError::new("read-only set.")) + } } #[derive(Debug, Copy, Hash, Eq, Clone, Serialize, Deserialize, PartialEq)] diff --git a/melib/src/backends/imap.rs b/melib/src/backends/imap.rs index e349b2c86..ea2a66007 100644 --- a/melib/src/backends/imap.rs +++ b/melib/src/backends/imap.rs @@ -153,6 +153,7 @@ impl MailBackend for ImapType { let handle = { let tx = w.tx(); let uid_store = self.uid_store.clone(); + let tag_index = self.tag_index.clone(); let can_create_flags = self.can_create_flags.clone(); let folder_path = folder.path().to_string(); let folder_hash = folder.hash(); @@ -209,6 +210,7 @@ impl MailBackend for ImapType { conn.read_response(&mut response) ); + let mut tag_lck = tag_index.write().unwrap(); while exists > 1 { let mut envelopes = vec![]; exit_on_error!(&tx, @@ -231,8 +233,15 @@ impl MailBackend for ImapType { h.write_usize(uid); h.write(folder_path.as_bytes()); env.set_hash(h.finish()); - if let Some(flags) = flags { + if let Some((flags, keywords)) = flags { env.set_flags(flags); + for f in keywords { + let hash = tag_hash!(f); + if !tag_lck.contains_key(&hash) { + tag_lck.insert(hash, f); + } + env.labels_mut().push(hash); + } } uid_store .hash_index @@ -272,6 +281,7 @@ impl MailBackend for ImapType { .capabilities .contains(&b"IDLE"[0..]); let folders = self.folders.clone(); + let tag_index = self.tag_index.clone(); let conn = ImapConnection::new_connection(&self.server_conf); let main_conn = self.connection.clone(); let is_online = self.online.clone(); @@ -292,6 +302,7 @@ impl MailBackend for ImapType { folders, sender, work_context, + tag_index, }; if has_idle { idle(kit).ok().take(); @@ -338,6 +349,7 @@ impl MailBackend for ImapType { .to_string(), self.connection.clone(), self.uid_store.clone(), + self.tag_index.clone(), )) } diff --git a/melib/src/backends/imap/operations.rs b/melib/src/backends/imap/operations.rs index ed7b43f0e..10aab9aa9 100644 --- a/melib/src/backends/imap/operations.rs +++ b/melib/src/backends/imap/operations.rs @@ -38,6 +38,7 @@ pub struct ImapOp { flags: Cell>, connection: Arc>, uid_store: Arc, + tag_index: Arc>>, } impl ImapOp { @@ -46,6 +47,7 @@ impl ImapOp { folder_path: String, connection: Arc>, uid_store: Arc, + tag_index: Arc>>, ) -> Self { ImapOp { uid, @@ -56,6 +58,7 @@ impl ImapOp { folder_path, flags: Cell::new(None), uid_store, + tag_index, } } } @@ -89,7 +92,7 @@ impl BackendOp for ImapOp { .to_full_result() .map_err(MeliError::from) { - Ok(v) => { + Ok(mut v) => { if v.len() != 1 { debug!("responses len is {}", v.len()); /* TODO: Trigger cache invalidation here. */ @@ -98,11 +101,11 @@ impl BackendOp for ImapOp { self.uid ))); } - let (uid, flags, b) = v[0]; + let (uid, flags, b) = v.remove(0); assert_eq!(uid, self.uid); - if flags.is_some() { - self.flags.set(flags); - cache.flags = flags; + if let Some((flags, _)) = flags { + self.flags.set(Some(flags)); + cache.flags = Some(flags); } cache.bytes = Some(unsafe { std::str::from_utf8_unchecked(b).to_string() }); } @@ -147,7 +150,7 @@ impl BackendOp for ImapOp { /* TODO: Trigger cache invalidation here. */ panic!(format!("message with UID {} was not found", self.uid)); } - let (uid, flags) = v[0]; + let (uid, (flags, _)) = v[0]; assert_eq!(uid, self.uid); cache.flags = Some(flags); self.flags.set(Some(flags)); @@ -184,7 +187,7 @@ impl BackendOp for ImapOp { Ok(v) => { if v.len() == 1 { debug!("responses len is {}", v.len()); - let (uid, flags) = v[0]; + let (uid, (flags, _)) = v[0]; assert_eq!(uid, self.uid); self.flags.set(Some(flags)); } @@ -196,4 +199,41 @@ impl BackendOp for ImapOp { cache.flags = Some(flags); Ok(()) } + + fn set_tag(&mut self, envelope: &mut Envelope, tag: String, value: bool) -> Result<()> { + let mut response = String::with_capacity(8 * 1024); + let mut conn = self.connection.lock().unwrap(); + conn.send_command(format!("SELECT \"{}\"", &self.folder_path,).as_bytes())?; + conn.read_response(&mut response)?; + conn.send_command( + format!( + "UID STORE {} {}FLAGS.SILENT ({})", + self.uid, + if value { "+" } else { "-" }, + &tag + ) + .as_bytes(), + )?; + conn.read_response(&mut response)?; + protocol_parser::uid_fetch_flags_response(response.as_bytes()) + .to_full_result() + .map_err(MeliError::from)?; + let hash = tag_hash!(tag); + if value { + self.tag_index.write().unwrap().insert(hash, tag); + } else { + self.tag_index.write().unwrap().remove(&hash); + } + if !envelope.labels().iter().any(|&h_| h_ == hash) { + if value { + envelope.labels_mut().push(hash); + } + } + if !value { + if let Some(pos) = envelope.labels().iter().position(|&h_| h_ == hash) { + envelope.labels_mut().remove(pos); + } + } + Ok(()) + } } diff --git a/melib/src/backends/imap/protocol_parser.rs b/melib/src/backends/imap/protocol_parser.rs index dd8cfdaae..abcc76996 100644 --- a/melib/src/backends/imap/protocol_parser.rs +++ b/melib/src/backends/imap/protocol_parser.rs @@ -108,7 +108,7 @@ named!( * "* 1 FETCH (FLAGS (\Seen) UID 1 RFC822.HEADER {5224} " */ named!( - pub uid_fetch_response, &[u8])>>, + pub uid_fetch_response)>, &[u8])>>, many0!( do_parse!( tag!("* ") @@ -124,7 +124,7 @@ named!( ); named!( - pub uid_fetch_flags_response>, + pub uid_fetch_flags_response))>>, many0!( do_parse!( tag!("* ") @@ -297,11 +297,11 @@ named!( pub struct SelectResponse { pub exists: usize, pub recent: usize, - pub flags: Flag, + pub flags: (Flag, Vec), pub unseen: usize, pub uidvalidity: usize, pub uidnext: usize, - pub permanentflags: Flag, + pub permanentflags: (Flag, Vec), /// if SELECT returns \* we can set arbritary flags permanently. pub can_create_flags: bool, pub read_only: bool, @@ -366,8 +366,9 @@ pub fn select_response(input: &str) -> Result { } } -pub fn flags(input: &str) -> IResult<&str, Flag> { +pub fn flags(input: &str) -> IResult<&str, (Flag, Vec)> { let mut ret = Flag::default(); + let mut keywords = Vec::new(); let mut input = input; while !input.starts_with(")") && !input.is_empty() { @@ -399,16 +400,16 @@ pub fn flags(input: &str) -> IResult<&str, Flag> { ret.set(Flag::DRAFT, true); } f => { - debug!("unknown Flag token value: {}", f); + keywords.push(f.to_string()); } } input = &input[match_end..]; input = input.trim_start(); } - IResult::Done(input, ret) + IResult::Done(input, (ret, keywords)) } -pub fn byte_flags(input: &[u8]) -> IResult<&[u8], Flag> { +pub fn byte_flags(input: &[u8]) -> IResult<&[u8], (Flag, Vec)> { let i = unsafe { std::str::from_utf8_unchecked(input) }; match flags(i) { IResult::Done(rest, ret) => IResult::Done(rest.as_bytes(), ret), @@ -587,7 +588,7 @@ named!( alt_complete!(map!(ws!(tag!("NIL")), |_| None) | map!(quoted, |v| Some(v)))); named!( - pub uid_fetch_envelopes_response, Envelope)>>, + pub uid_fetch_envelopes_response)>, Envelope)>>, many0!( do_parse!( tag!("* ") diff --git a/melib/src/backends/imap/watch.rs b/melib/src/backends/imap/watch.rs index a03a75387..9c69da03a 100644 --- a/melib/src/backends/imap/watch.rs +++ b/melib/src/backends/imap/watch.rs @@ -30,6 +30,7 @@ pub struct ImapWatchKit { pub folders: Arc>>, pub sender: RefreshEventConsumer, pub work_context: WorkContext, + pub tag_index: Arc>>, } macro_rules! exit_on_error { @@ -57,6 +58,7 @@ pub fn poll_with_examine(kit: ImapWatchKit) -> Result<()> { folders, sender, work_context, + tag_index, } = kit; loop { if *is_online.lock().unwrap() { @@ -81,7 +83,14 @@ pub fn poll_with_examine(kit: ImapWatchKit) -> Result<()> { format!("examining `{}` for updates...", folder.path()), )) .unwrap(); - examine_updates(folder, &sender, &mut conn, &uid_store, &work_context)?; + examine_updates( + folder, + &sender, + &mut conn, + &uid_store, + &work_context, + &tag_index, + )?; } let mut main_conn = main_conn.lock().unwrap(); main_conn.send_command(b"NOOP").unwrap(); @@ -101,6 +110,7 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> { folders, sender, work_context, + tag_index, } = kit; loop { if *is_online.lock().unwrap() { @@ -236,7 +246,14 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> { folder_hash, work_context, thread_id, - examine_updates(folder, &sender, &mut conn, &uid_store, &work_context) + examine_updates( + folder, + &sender, + &mut conn, + &uid_store, + &work_context, + &tag_index + ) ); } work_context @@ -301,7 +318,9 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> { format!("parsing {}/{} envelopes..", ctr, len), )) .unwrap(); - if let Ok(env) = Envelope::from_bytes(&b, flags) { + if let Ok(mut env) = + Envelope::from_bytes(&b, flags.as_ref().map(|&(f, _)| f)) + { ctr += 1; uid_store .hash_index @@ -315,6 +334,16 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> { env.subject(), folder.path(), ); + if let Some((_, keywords)) = flags { + let mut tag_lck = tag_index.write().unwrap(); + for f in keywords { + let hash = tag_hash!(f); + if !tag_lck.contains_key(&hash) { + tag_lck.insert(hash, f); + } + env.labels_mut().push(hash); + } + } sender.send(RefreshEvent { hash: folder_hash, kind: Create(Box::new(env)), @@ -402,7 +431,9 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> { ctr += 1; continue; } - if let Ok(env) = Envelope::from_bytes(&b, flags) { + if let Ok(mut env) = + Envelope::from_bytes(&b, flags.as_ref().map(|&(f, _)| f)) + { ctr += 1; uid_store .hash_index @@ -410,6 +441,16 @@ pub fn idle(kit: ImapWatchKit) -> Result<()> { .unwrap() .insert(env.hash(), (uid, folder_hash)); uid_store.uid_index.lock().unwrap().insert(uid, env.hash()); + if let Some((_, keywords)) = flags { + let mut tag_lck = tag_index.write().unwrap(); + for f in keywords { + let hash = tag_hash!(f); + if !tag_lck.contains_key(&hash) { + tag_lck.insert(hash, f); + } + env.labels_mut().push(hash); + } + } debug!( "Create event {} {} {}", env.hash(), @@ -467,6 +508,7 @@ fn examine_updates( conn: &mut ImapConnection, uid_store: &Arc, work_context: &WorkContext, + tag_index: &Arc>>, ) -> Result<()> { let thread_id: std::thread::ThreadId = std::thread::current().id(); let folder_hash = folder.hash(); @@ -551,7 +593,10 @@ fn examine_updates( { Ok(v) => { for (uid, flags, b) in v { - if let Ok(env) = Envelope::from_bytes(&b, flags) { + if let Ok(mut env) = Envelope::from_bytes( + &b, + flags.as_ref().map(|&(f, _)| f), + ) { uid_store .hash_index .lock() @@ -568,6 +613,16 @@ fn examine_updates( env.subject(), folder.path(), ); + if let Some((_, keywords)) = flags { + let mut tag_lck = tag_index.write().unwrap(); + for f in keywords { + let hash = tag_hash!(f); + if !tag_lck.contains_key(&hash) { + tag_lck.insert(hash, f); + } + env.labels_mut().push(hash); + } + } sender.send(RefreshEvent { hash: folder_hash, kind: Create(Box::new(env)), @@ -614,13 +669,25 @@ fn examine_updates( { Ok(v) => { for (uid, flags, b) in v { - if let Ok(env) = Envelope::from_bytes(&b, flags) { + if let Ok(mut env) = + Envelope::from_bytes(&b, flags.as_ref().map(|&(f, _)| f)) + { uid_store .hash_index .lock() .unwrap() .insert(env.hash(), (uid, folder_hash)); uid_store.uid_index.lock().unwrap().insert(uid, env.hash()); + if let Some((_, keywords)) = flags { + let mut tag_lck = tag_index.write().unwrap(); + for f in keywords { + let hash = tag_hash!(f); + if !tag_lck.contains_key(&hash) { + tag_lck.insert(hash, f); + } + env.labels_mut().push(hash); + } + } debug!( "Create event {} {} {}", env.hash(), diff --git a/melib/src/backends/maildir.rs b/melib/src/backends/maildir.rs index 4c939149f..b2bbe5b6a 100644 --- a/melib/src/backends/maildir.rs +++ b/melib/src/backends/maildir.rs @@ -162,6 +162,10 @@ impl<'a> BackendOp for MaildirOp { debug!("success in rename"); Ok(()) } + + fn set_tag(&mut self, _envelope: &mut Envelope, _tag: String, _value: bool) -> Result<()> { + Err(MeliError::new("Maildir doesn't support tags.")) + } } #[derive(Debug, Default)] diff --git a/melib/src/backends/mbox.rs b/melib/src/backends/mbox.rs index 22e7f4b50..a69e10106 100644 --- a/melib/src/backends/mbox.rs +++ b/melib/src/backends/mbox.rs @@ -241,6 +241,10 @@ impl BackendOp for MboxOp { fn set_flag(&mut self, _envelope: &mut Envelope, _flag: Flag, _value: bool) -> Result<()> { Ok(()) } + + fn set_tag(&mut self, _envelope: &mut Envelope, _tag: String, _value: bool) -> Result<()> { + Err(MeliError::new("mbox doesn't support tags.")) + } } pub fn mbox_parse( diff --git a/melib/src/backends/notmuch.rs b/melib/src/backends/notmuch.rs index dced8b7b1..70bac2b53 100644 --- a/melib/src/backends/notmuch.rs +++ b/melib/src/backends/notmuch.rs @@ -11,7 +11,7 @@ use crate::structs::StackVec; use fnv::FnvHashMap; use std::collections::hash_map::DefaultHasher; use std::collections::BTreeMap; -use std::ffi::CStr; +use std::ffi::{CStr, CString}; use std::hash::{Hash, Hasher}; use std::io::Read; use std::path::{Path, PathBuf}; @@ -313,6 +313,7 @@ impl MailBackend for NotmuchDb { hash: env_hash, index: index.clone(), bytes: Some(response), + tag_index: tag_index.clone(), }); if let Some(mut env) = Envelope::from_token(op, env_hash) { let mut tag_lock = tag_index.write().unwrap(); @@ -372,6 +373,7 @@ impl MailBackend for NotmuchDb { hash, index: self.index.clone(), bytes: None, + tag_index: self.tag_index.clone(), }) } @@ -410,6 +412,7 @@ impl MailBackend for NotmuchDb { struct NotmuchOp { hash: EnvelopeHash, index: Arc>>, + tag_index: Arc>>, database: DbWrapper, bytes: Option, } @@ -547,6 +550,58 @@ impl BackendOp for NotmuchOp { Ok(()) } + + fn set_tag(&mut self, envelope: &mut Envelope, tag: String, value: bool) -> Result<()> { + let mut message: *mut notmuch_message_t = std::ptr::null_mut(); + let index_lck = self.index.read().unwrap(); + unsafe { + notmuch_database_find_message_by_filename( + *self.database.inner.read().unwrap(), + index_lck[&self.hash].as_ptr(), + &mut message as *mut _, + ) + }; + if message.is_null() { + return Err(MeliError::new(format!( + "Error, message with path {:?} not found in notmuch database.", + index_lck[&self.hash] + ))); + } + if value { + if unsafe { + notmuch_message_add_tag(message, CString::new(tag.as_str()).unwrap().as_ptr()) + } != _notmuch_status_NOTMUCH_STATUS_SUCCESS + { + return Err(MeliError::new("Could not set tag.")); + } + debug!("added tag {}", &tag); + } else { + if unsafe { + notmuch_message_remove_tag(message, CString::new(tag.as_str()).unwrap().as_ptr()) + } != _notmuch_status_NOTMUCH_STATUS_SUCCESS + { + return Err(MeliError::new("Could not set tag.")); + } + debug!("removed tag {}", &tag); + } + let hash = tag_hash!(tag); + if value { + self.tag_index.write().unwrap().insert(hash, tag); + } else { + self.tag_index.write().unwrap().remove(&hash); + } + if !envelope.labels().iter().any(|&h_| h_ == hash) { + if value { + envelope.labels_mut().push(hash); + } + } + if !value { + if let Some(pos) = envelope.labels().iter().position(|&h_| h_ == hash) { + envelope.labels_mut().remove(pos); + } + } + Ok(()) + } } pub struct MessageIterator { diff --git a/ui/src/cache.rs b/ui/src/cache.rs index f0e7530f2..3f8dbfce0 100644 --- a/ui/src/cache.rs +++ b/ui/src/cache.rs @@ -154,7 +154,10 @@ pub mod query_parser { fn flags<'a>() -> impl Parser<'a, Query> { move |input| { whitespace_wrap(either( - match_literal_anycase("flags:"), + either( + match_literal_anycase("flags:"), + match_literal_anycase("tags:"), + ), match_literal_anycase("is:"), )) .parse(input) @@ -322,6 +325,10 @@ pub mod query_parser { Ok(("", Flags(vec!["test".to_string(), "testtest".to_string()]))), query().parse_complete("flags:test,testtest") ); + assert_eq!( + query().parse_complete("flags:test,testtest"), + query().parse_complete("tags:test,testtest") + ); } } @@ -387,7 +394,7 @@ pub fn query_to_imap(q: &Query) -> String { s.push_str(" UNANSWERED "); } keyword => { - s.push_str(" "); + s.push_str(" KEYWORD "); s.extend(keyword.chars()); s.push_str(" "); } diff --git a/ui/src/components/mail/listing.rs b/ui/src/components/mail/listing.rs index ad53231a2..4b7370879 100644 --- a/ui/src/components/mail/listing.rs +++ b/ui/src/components/mail/listing.rs @@ -105,42 +105,27 @@ pub trait MailListingTrait: ListingTrait { continue; } ListingAction::Tag(Remove(ref tag_str)) => { - use std::collections::hash_map::DefaultHasher; - use std::hash::Hasher; - let h = { - let mut hasher = DefaultHasher::new(); - hasher.write(tag_str.as_bytes()); - hasher.finish() - }; let backend_lck = account.backend.write().unwrap(); - if let Some(t) = backend_lck.tags() { - let mut tags_lck = t.write().unwrap(); - if !tags_lck.contains_key(&h) { - tags_lck.insert(h, tag_str.to_string()); - } - if let Some(pos) = envelope.labels().iter().position(|&el| el == h) { - envelope.labels_mut().remove(pos); - } - } else { + let mut op = backend_lck.operation(envelope.hash()); + if let Err(err) = op.set_tag(&mut envelope, tag_str.to_string(), false) { + context.replies.push_back(UIEvent::Notification( + Some("Could not set tag.".to_string()), + err.to_string(), + Some(NotificationType::ERROR), + )); return; } } ListingAction::Tag(Add(ref tag_str)) => { - use std::collections::hash_map::DefaultHasher; - use std::hash::Hasher; - let h = { - let mut hasher = DefaultHasher::new(); - hasher.write(tag_str.as_bytes()); - hasher.finish() - }; let backend_lck = account.backend.write().unwrap(); - if let Some(t) = backend_lck.tags() { - let mut tags_lck = t.write().unwrap(); - if !tags_lck.contains_key(&h) { - tags_lck.insert(h, tag_str.to_string()); - } - envelope.labels_mut().push(h); - } else { + let mut op = backend_lck.operation(envelope.hash()); + + if let Err(err) = op.set_tag(&mut envelope, tag_str.to_string(), true) { + context.replies.push_back(UIEvent::Notification( + Some("Could not set tag.".to_string()), + err.to_string(), + Some(NotificationType::ERROR), + )); return; } } diff --git a/ui/src/components/mail/listing/conversations.rs b/ui/src/components/mail/listing/conversations.rs index b8019871a..0ba9306db 100644 --- a/ui/src/components/mail/listing/conversations.rs +++ b/ui/src/components/mail/listing/conversations.rs @@ -1131,6 +1131,9 @@ impl Component for ConversationsListing { area, Some(get_x(upper_left)), ); + for c in grid.row_iter((x, get_x(bottom_right)), y) { + grid[c] = Cell::default(); + } clear_area(grid, ((x, y), set_y(bottom_right, y))); context .dirty_areas diff --git a/ui/src/terminal/cells.rs b/ui/src/terminal/cells.rs index c74ab4516..ba88e9ca7 100644 --- a/ui/src/terminal/cells.rs +++ b/ui/src/terminal/cells.rs @@ -1576,9 +1576,6 @@ pub fn write_string_to_grid( if c == '\r' { continue; } - grid[(x, y)].set_attrs(attrs); - grid[(x, y)].set_fg(fg_color); - grid[(x, y)].set_bg(bg_color); if c == '\t' { grid[(x, y)].set_ch(' '); x += 1; @@ -1587,6 +1584,9 @@ pub fn write_string_to_grid( } else { grid[(x, y)].set_ch(c); } + grid[(x, y)].set_attrs(attrs); + grid[(x, y)].set_fg(fg_color); + grid[(x, y)].set_bg(bg_color); match wcwidth(u32::from(c)) { Some(0) | None => { @@ -1598,6 +1598,10 @@ pub fn write_string_to_grid( * drawn over. Set it as empty to skip drawing it. */ x += 1; inspect_bounds!(grid, area, x, y, line_break); + grid[(x, y)] = Cell::default(); + grid[(x, y)].set_attrs(attrs); + grid[(x, y)].set_fg(fg_color); + grid[(x, y)].set_bg(bg_color); grid[(x, y)].empty = true; } _ => {}