Fix proper viewing for multipart alternatives, html view and quoted printable soft breaks

embed
Manos Pitsidianakis 2018-08-08 19:06:00 +03:00
parent 93b36a9941
commit 14d65838b7
Signed by: Manos Pitsidianakis
GPG Key ID: 73627C2F690DF710
7 changed files with 240 additions and 125 deletions

View File

@ -83,15 +83,39 @@ impl Display for ContentType {
} }
} }
} }
impl ContentType {
pub fn is_text(&self) -> bool {
if let ContentType::Text { .. } = self {
true
} else {
false
}
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum ContentSubType { pub enum ContentSubType {
Plain, Plain,
Html,
Other { tag: Vec<u8> }, Other { tag: Vec<u8> },
} }
impl ContentSubType {
pub fn is_html(&self) -> bool {
if let ContentSubType::Html = self {
true
} else {
false
}
}
}
impl Display for ContentSubType { impl Display for ContentSubType {
fn fmt(&self, f: &mut Formatter) -> FmtResult { fn fmt(&self, f: &mut Formatter) -> FmtResult {
match *self { match *self {
ContentSubType::Plain => write!(f, "plain"), ContentSubType::Plain => write!(f, "plain"),
ContentSubType::Html => write!(f, "html"),
ContentSubType::Other { tag: ref t } => write!(f, "{}", String::from_utf8_lossy(t)), ContentSubType::Other { tag: ref t } => write!(f, "{}", String::from_utf8_lossy(t)),
} }
} }

View File

@ -18,14 +18,14 @@
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with meli. If not, see <http://www.gnu.org/licenses/>. * along with meli. If not, see <http://www.gnu.org/licenses/>.
*/ */
use std::fmt::{Display, Formatter, Result as FmtResult}; use std::fmt;
use std::str; use std::str;
use data_encoding::BASE64_MIME; use data_encoding::BASE64_MIME;
use mailbox::email::parser; use mailbox::email::parser;
pub use mailbox::email::attachment_types::*; pub use mailbox::email::attachment_types::*;
#[derive(Clone, Debug)] #[derive(Clone)]
pub enum AttachmentType { pub enum AttachmentType {
Data { Data {
tag: Vec<u8>, tag: Vec<u8>,
@ -39,6 +39,16 @@ pub enum AttachmentType {
}, },
} }
impl fmt::Debug for AttachmentType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
AttachmentType::Data { .. } => write!(f, "AttachmentType::Data {{ .. }}"),
AttachmentType::Text { .. } => write!(f, "AttachmentType::Text {{ .. }}"),
AttachmentType::Multipart { of_type, subattachments } => write!(f, "AttachmentType::Multipart {{ of_type: {:?},\nsubattachments: {:?} }}", of_type, subattachments),
}
}
}
/* /*
* *
* Data * Data
@ -54,17 +64,27 @@ pub struct AttachmentBuilder {
raw: Vec<u8>, raw: Vec<u8>,
} }
#[derive(Clone, Debug)] #[derive(Clone)]
pub struct Attachment { pub struct Attachment {
content_type: (ContentType, ContentSubType), content_type: (ContentType, ContentSubType),
content_transfer_encoding: ContentTransferEncoding, content_transfer_encoding: ContentTransferEncoding,
raw: Vec<u8>, raw: Vec<u8>,
attachment_type: AttachmentType, attachment_type: AttachmentType,
} }
impl Display for AttachmentType {
fn fmt(&self, f: &mut Formatter) -> FmtResult { impl fmt::Debug for Attachment {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Attachment {{\n content_type: {:?},\n content_transfer_encoding: {:?},\n raw: Vec of {} bytes\n attachment_type: {:?}\n }}",
self.content_type,
self.content_transfer_encoding,
self.raw.len(),
self.attachment_type)
}
}
impl fmt::Display for AttachmentType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self { match self {
AttachmentType::Data { tag: ref t } => write!(f, "{}", String::from_utf8_lossy(t)), AttachmentType::Data { tag: ref t } => write!(f, "{}", String::from_utf8_lossy(t)),
AttachmentType::Text { content: ref c } => write!(f, "{}", String::from_utf8_lossy(c)), AttachmentType::Text { content: ref c } => write!(f, "{}", String::from_utf8_lossy(c)),
@ -108,7 +128,9 @@ impl AttachmentBuilder {
break; break;
} }
} }
if !cst.eq_ignore_ascii_case(b"plain") { if cst.eq_ignore_ascii_case(b"html") {
self.content_type.1 = ContentSubType::Html;
} else if !cst.eq_ignore_ascii_case(b"plain") {
self.content_type.1 = ContentSubType::Other { self.content_type.1 = ContentSubType::Other {
tag: cst.to_ascii_lowercase(), tag: cst.to_ascii_lowercase(),
}; };
@ -144,25 +166,27 @@ impl AttachmentBuilder {
self self
} }
fn decode(&self) -> Vec<u8> { fn decode(&self) -> Vec<u8> {
// TODO merge this and standalone decode() function
let charset = match self.content_type.0 { let charset = match self.content_type.0 {
ContentType::Text{ charset: c } => c, ContentType::Text{ charset: c } => c,
_ => Default::default(), _ => Default::default(),
}; };
let decoded_result = parser::decode_charset(&self.raw, charset);
let b: &[u8] = decoded_result.as_ref().map(|v| v.as_bytes()).unwrap_or_else(|_| &self.raw);
match self.content_transfer_encoding { let bytes = match self.content_transfer_encoding {
ContentTransferEncoding::Base64 => match BASE64_MIME.decode(b) { ContentTransferEncoding::Base64 => match BASE64_MIME.decode(&self.raw) {
Ok(v) => v, Ok(v) => v,
_ => b.to_vec(), _ => self.raw.to_vec(),
}, },
ContentTransferEncoding::QuotedPrintable => parser::quoted_printable_text(b) ContentTransferEncoding::QuotedPrintable => parser::quoted_printable_bytes(&self.raw)
.to_full_result() .to_full_result()
.unwrap(), .unwrap(),
ContentTransferEncoding::_7Bit ContentTransferEncoding::_7Bit
| ContentTransferEncoding::_8Bit | ContentTransferEncoding::_8Bit
| ContentTransferEncoding::Other { .. } => b.to_vec(), | ContentTransferEncoding::Other { .. } => self.raw.to_vec(),
} };
let decoded_result = parser::decode_charset(&bytes, charset);
decoded_result.as_ref().map(|v| v.as_bytes()).unwrap_or_else(|_| &self.raw).to_vec()
} }
pub fn build(self) -> Attachment { pub fn build(self) -> Attachment {
let attachment_type = match self.content_type.0 { let attachment_type = match self.content_type.0 {
@ -172,9 +196,9 @@ impl AttachmentBuilder {
ContentType::Multipart { boundary: ref b } => { ContentType::Multipart { boundary: ref b } => {
let multipart_type = match self.content_type.1 { let multipart_type = match self.content_type.1 {
ContentSubType::Other { ref tag } => match &tag[..] { ContentSubType::Other { ref tag } => match &tag[..] {
b"mixed" => MultipartType::Mixed, b"mixed" | b"Mixed" | b"MIXED" => MultipartType::Mixed,
b"alternative" => MultipartType::Alternative, b"alternative" | b"Alternative" | b"ALTERNATIVE" => MultipartType::Alternative,
b"digest" => MultipartType::Digest, b"digest" | b"Digest" | b"DIGEST" => MultipartType::Digest,
_ => MultipartType::Unsupported { tag: tag.clone() }, _ => MultipartType::Unsupported { tag: tag.clone() },
}, },
_ => panic!(), _ => panic!(),
@ -238,8 +262,8 @@ impl AttachmentBuilder {
} }
impl Display for Attachment { impl fmt::Display for Attachment {
fn fmt(&self, f: &mut Formatter) -> FmtResult { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.attachment_type { match self.attachment_type {
AttachmentType::Data { .. } => { AttachmentType::Data { .. } => {
write!(f, "Data attachment of type {}", self.mime_type()) write!(f, "Data attachment of type {}", self.mime_type())
@ -265,13 +289,13 @@ impl Attachment {
pub fn bytes(&self) -> &[u8] { pub fn bytes(&self) -> &[u8] {
&self.raw &self.raw
} }
fn get_text_recursive(&self, text: &mut String) { fn get_text_recursive(&self, text: &mut Vec<u8>) {
match self.attachment_type { match self.attachment_type {
AttachmentType::Data { .. } => { AttachmentType::Data { .. } => {
//text.push_str(&format!("Data attachment of type {}", self.mime_type())); //text.push_str(&format!("Data attachment of type {}", self.mime_type()));
} }
AttachmentType::Text { content: ref t } => { AttachmentType::Text { .. } => {
text.push_str(&String::from_utf8_lossy(t)); text.extend(decode(self, None));
} }
AttachmentType::Multipart { AttachmentType::Multipart {
of_type: ref multipart_type, of_type: ref multipart_type,
@ -286,15 +310,15 @@ impl Attachment {
} else { } else {
for a in sub_att_vec { for a in sub_att_vec {
a.get_text_recursive(text); a.get_text_recursive(text);
text.push_str("\n\n"); text.extend_from_slice(b"\n\n");
} }
}, },
} }
} }
pub fn text(&self) -> String { pub fn text(&self) -> String {
let mut text = String::with_capacity(self.raw.len()); let mut text = Vec::with_capacity(self.raw.len());
self.get_text_recursive(&mut text); self.get_text_recursive(&mut text);
text String::from_utf8_lossy(&text).into()
} }
pub fn description(&self) -> Vec<String> { pub fn description(&self) -> Vec<String> {
self.attachments().iter().map(|a| a.text()).collect() self.attachments().iter().map(|a| a.text()).collect()
@ -341,24 +365,66 @@ pub fn interpret_format_flowed(_t: &str) -> String {
unimplemented!() unimplemented!()
} }
pub fn decode(a: &Attachment) -> Vec<u8> { fn decode_rec_helper(a: &Attachment, filter: &Option<Box<Fn(&Attachment) -> Vec<u8>>>) -> Vec<u8> {
if let Some(filter) = filter {
return filter(a);
}
match a.attachment_type {
AttachmentType::Data { .. } => { Vec::new()},
AttachmentType::Text { .. } => decode_helper(a, filter),
AttachmentType::Multipart {
of_type: ref multipart_type,
subattachments: ref sub_att_vec,
} => if *multipart_type == MultipartType::Alternative {
for a in sub_att_vec {
if a.content_type.1 == ContentSubType::Plain {
return decode_helper(a, filter);
}
}
decode_helper(a, filter)
} else {
let mut vec = Vec::new();
for a in sub_att_vec {
vec.extend(decode_rec_helper(a, filter));
vec.extend_from_slice(b"\n\n");
}
vec
},
}
}
pub fn decode_rec(a: &Attachment, filter: Option<Box<Fn(&Attachment) -> Vec<u8>>>) -> Vec<u8> {
decode_rec_helper(a, &filter)
}
fn decode_helper(a: &Attachment, filter: &Option<Box<Fn(&Attachment) -> Vec<u8>>>) -> Vec<u8> {
if let Some(filter) = filter {
return filter(a);
}
let charset = match a.content_type.0 { let charset = match a.content_type.0 {
ContentType::Text{ charset: c } => c, ContentType::Text{ charset: c } => c,
_ => Default::default(), _ => Default::default(),
}; };
let decoded_result = parser::decode_charset(a.bytes(), charset);
let b: &[u8] = decoded_result.as_ref().map(|v| v.as_bytes()).unwrap_or_else(|_| a.bytes()); let bytes = match a.content_transfer_encoding {
ContentTransferEncoding::Base64 => match BASE64_MIME.decode(a.bytes()) {
match a.content_transfer_encoding {
ContentTransferEncoding::Base64 => match BASE64_MIME.decode(b) {
Ok(v) => v, Ok(v) => v,
_ => b.to_vec(), _ => a.bytes().to_vec(),
}, },
ContentTransferEncoding::QuotedPrintable => parser::quoted_printed_bytes(b) ContentTransferEncoding::QuotedPrintable => parser::quoted_printable_bytes(a.bytes())
.to_full_result() .to_full_result()
.unwrap(), .unwrap(),
ContentTransferEncoding::_7Bit ContentTransferEncoding::_7Bit
| ContentTransferEncoding::_8Bit | ContentTransferEncoding::_8Bit
| ContentTransferEncoding::Other { .. } => b.to_vec(), | ContentTransferEncoding::Other { .. } => a.bytes().to_vec(),
};
if a.content_type().0.is_text() {
let decoded_result = parser::decode_charset(&bytes, charset);
decoded_result.as_ref().map(|v| v.as_bytes()).unwrap_or_else(|_| a.bytes()).to_vec()
} else {
bytes.to_vec()
} }
} }
pub fn decode(a: &Attachment, filter: Option<Box<Fn(&Attachment) -> Vec<u8>>>) -> Vec<u8> {
decode_helper(a, &filter)
}

View File

@ -206,7 +206,7 @@ fn encoded_word(input: &[u8]) -> IResult<&[u8], Vec<u8>> {
Ok(v) => v, Ok(v) => v,
Err(_) => encoded.to_vec(), Err(_) => encoded.to_vec(),
}, },
b'q' | b'Q' => match quoted_printed_bytes(encoded) { b'q' | b'Q' => match quoted_printable_bytes_header(encoded) {
IResult::Done(b"", s) => s, IResult::Done(b"", s) => s,
_ => return IResult::Error(error_code!(ErrorKind::Custom(43))), _ => return IResult::Error(error_code!(ErrorKind::Custom(43))),
}, },
@ -296,16 +296,38 @@ pub fn decode_charset(s: &[u8], charset: Charset) -> Result<String> {
} }
} }
fn quoted_printable_soft_break(input: &[u8]) -> IResult<&[u8], &[u8]> {
if input.len() < 2 {
IResult::Incomplete(Needed::Size(1))
} else if input[0] == b'=' && input[1] == b'\n' {
IResult::Done(&input[2..], &input[0..2]) // `=\n` is an escaped space character.
} else {
IResult::Error(error_code!(ErrorKind::Custom(43)))
}
}
named!(qp_underscore_header<u8>, do_parse!(tag!("_") >> ({ b' ' }))); named!(qp_underscore_header<u8>, do_parse!(tag!("_") >> ({ b' ' })));
/// For atoms in Header values. // With MIME, headers in quoted printable format can contain underscores that represent spaces.
// In non-header context, an underscore is just a plain underscore.
named!( named!(
pub quoted_printed_bytes<Vec<u8>>, pub quoted_printable_bytes_header<Vec<u8>>,
many0!(alt_complete!( many0!(alt_complete!(
quoted_printable_byte | qp_underscore_header | le_u8 quoted_printable_byte | qp_underscore_header | le_u8
)) ))
); );
/// For atoms in Header values.
named!(
pub quoted_printable_bytes<Vec<u8>>,
many0!(alt_complete!(
preceded!(quoted_printable_soft_break, quoted_printable_byte) |
preceded!(quoted_printable_soft_break, le_u8)
| quoted_printable_byte | le_u8
))
);
named!( named!(
encoded_word_list<Vec<u8>>, encoded_word_list<Vec<u8>>,
ws!(do_parse!( ws!(do_parse!(
@ -527,11 +549,7 @@ fn test_address() {
println!("{:?}", rfc2822address_list(s).unwrap()); println!("{:?}", rfc2822address_list(s).unwrap());
} }
named!(pub rfc2822address_list<Vec<Address>>, ws!( named!(pub rfc2822address_list<Vec<Address>>, ws!( separated_list!(is_a!(","), address)));
separated_list!(is_a!(","), address)
));
named!(pub address_list<String>, ws!(do_parse!( named!(pub address_list<String>, ws!(do_parse!(
list: alt_complete!( encoded_word_list | ascii_token) >> list: alt_complete!( encoded_word_list | ascii_token) >>
@ -766,15 +784,15 @@ named!(pub content_type< (&[u8], &[u8], Vec<(&[u8], &[u8])>) >,
} ) } )
)); ));
named!(pub quoted_printable_text<Vec<u8>>, //named!(pub quoted_printable_text<Vec<u8>>,
do_parse!( // do_parse!(
bytes: many0!(alt_complete!( // bytes: many0!(alt_complete!(
preceded!(tag!("=\n"), quoted_printable_byte) | // preceded!(tag!("=\n"), quoted_printable_byte) |
preceded!(tag!("=\n"), le_u8) | // preceded!(tag!("=\n"), le_u8) |
quoted_printable_byte | // quoted_printable_byte |
le_u8)) >> // le_u8)) >>
( { // ( {
bytes // bytes
} ) // } )
) // )
); //);

View File

@ -70,6 +70,78 @@ impl MailView {
cmd_buf: String::with_capacity(4), cmd_buf: String::with_capacity(4),
} }
} }
/// Returns the string to be displayed in the Viewer
fn attachment_to_text(&self, envelope: &Envelope) -> String {
let finder = LinkFinder::new();
let body = envelope.body();
let body_text = if body.content_type().0.is_text() && body.content_type().1.is_html() {
String::from_utf8_lossy(&decode(&body, Some(Box::new(|a: &Attachment| {
use std::io::Write;
use std::process::{Command, Stdio};
let raw = decode(a, None);
let mut html_filter = Command::new("w3m")
.args(&["-I", "utf-8", "-T", "text/html"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect("Failed to start html filter process");
html_filter.stdin.as_mut().unwrap().write_all(&raw).expect("Failed to write to w3m stdin");
html_filter.wait_with_output().unwrap().stdout
})))).into_owned()
} else {
String::from_utf8_lossy(&decode_rec(&body, None)).into()
};
match self.mode {
ViewMode::Normal => {
let mut t = body_text.to_string();
if body.count_attachments() > 1 {
t = body.attachments().iter().enumerate().fold(
t,
|mut s, (idx, a)| {
s.push_str(&format!("[{}] {}\n\n", idx, a));
s
},
);
}
t
}
ViewMode::Raw => String::from_utf8_lossy(&envelope.bytes()).into_owned(),
ViewMode::Url => {
let mut t = body_text.to_string();
for (lidx, l) in finder.links(&body.text()).enumerate() {
let offset = if lidx < 10 {
lidx * 3
} else if lidx < 100 {
26 + (lidx - 9) * 4
} else if lidx < 1000 {
385 + (lidx - 99) * 5
} else {
panic!("BUG: Message body with more than 100 urls");
};
t.insert_str(l.start() + offset, &format!("[{}]", lidx));
}
if body.count_attachments() > 1 {
t = body.attachments().iter().enumerate().fold(
t,
|mut s, (idx, a)| {
s.push_str(&format!("[{}] {}\n\n", idx, a));
s
},
);
}
t
}
ViewMode::Attachment(aidx) => {
let attachments = body.attachments();
let mut ret = "Viewing attachment. Press `r` to return \n".to_string();
ret.push_str(&attachments[aidx].text());
ret
}
}
}
} }
impl Component for MailView { impl Component for MailView {
@ -177,61 +249,15 @@ impl Component for MailView {
.as_ref() .as_ref()
.unwrap(); .unwrap();
let envelope: &Envelope = &mailbox.collection[envelope_idx]; let envelope: &Envelope = &mailbox.collection[envelope_idx];
let text = self.attachment_to_text(envelope);
let finder = LinkFinder::new();
let mut text = match self.mode {
ViewMode::Normal => {
let mut t = envelope.body().text().to_string();
if envelope.body().count_attachments() > 1 {
t = envelope.body().attachments().iter().enumerate().fold(
t,
|mut s, (idx, a)| {
s.push_str(&format!("[{}] {}\n\n", idx, a));
s
},
);
}
t
}
ViewMode::Raw => String::from_utf8_lossy(&envelope.bytes()).into_owned(),
ViewMode::Url => {
let mut t = envelope.body().text().to_string();
for (lidx, l) in finder.links(&envelope.body().text()).enumerate() {
let offset = if lidx < 10 {
lidx * 3
} else if lidx < 100 {
26 + (lidx - 9) * 4
} else if lidx < 1000 {
385 + (lidx - 99) * 5
} else {
panic!("BUG: Message body with more than 100 urls");
};
t.insert_str(l.start() + offset, &format!("[{}]", lidx));
}
if envelope.body().count_attachments() > 1 {
t = envelope.body().attachments().iter().enumerate().fold(
t,
|mut s, (idx, a)| {
s.push_str(&format!("[{}] {}\n\n", idx, a));
s
},
);
}
t
}
ViewMode::Attachment(aidx) => {
let attachments = envelope.body().attachments();
let mut ret = "Viewing attachment. Press `r` to return \n".to_string();
ret.push_str(&attachments[aidx].text());
ret
}
};
let mut buf = CellBuffer::from(&text); let mut buf = CellBuffer::from(&text);
if self.mode == ViewMode::Url { if self.mode == ViewMode::Url {
// URL indexes must be colored (ugh..) // URL indexes must be colored (ugh..)
let lines: Vec<&str> = text.split('\n').map(|l| l.trim_right()).collect(); let lines: Vec<&str> = text.split('\n').map(|l| l.trim_right()).collect();
let mut shift = 0; let mut shift = 0;
let mut lidx_total = 0; let mut lidx_total = 0;
let finder = LinkFinder::new();
for r in &lines { for r in &lines {
for l in finder.links(&r) { for l in finder.links(&r) {
let offset = if lidx_total < 10 { let offset = if lidx_total < 10 {
@ -331,7 +357,7 @@ impl Component for MailView {
let attachment_type = u.mime_type(); let attachment_type = u.mime_type();
let binary = query_default_app(&attachment_type); let binary = query_default_app(&attachment_type);
if let Ok(binary) = binary { if let Ok(binary) = binary {
let mut p = create_temp_file(&decode(u), None); let mut p = create_temp_file(&decode(u, None), None);
Command::new(&binary) Command::new(&binary)
.arg(p.path()) .arg(p.path())
.stdin(Stdio::piped()) .stdin(Stdio::piped())

View File

@ -45,18 +45,6 @@ pub enum SortField {
impl FromStr for SortField { impl FromStr for SortField {
type Err = (); type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
eprintln!("sortfield from_str {}", s);
match s.trim() {
"subject" | "s" | "sub" | "sbj" | "subj" => {
eprintln!("parsed: subject");
}
"date" | "d" => {
eprintln!("parsed date");
}
_ => {
eprintln!("error in parse");
}
}
match s.trim() { match s.trim() {
"subject" | "s" | "sub" | "sbj" | "subj" => Ok(SortField::Subject), "subject" | "s" | "sub" | "sbj" | "subj" => Ok(SortField::Subject),
"date" | "d" => Ok(SortField::Date), "date" | "d" => Ok(SortField::Date),
@ -68,7 +56,6 @@ impl FromStr for SortField {
impl FromStr for SortOrder { impl FromStr for SortOrder {
type Err = (); type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
eprintln!("sortoder from_str {}", s);
match s.trim() { match s.trim() {
"asc" => Ok(SortOrder::Asc), "asc" => Ok(SortOrder::Asc),
"desc" => Ok(SortOrder::Desc), "desc" => Ok(SortOrder::Desc),

View File

@ -183,10 +183,6 @@ impl State<std::io::Stdout> {
account.watch(RefreshEventConsumer::new(Box::new(move |r| { account.watch(RefreshEventConsumer::new(Box::new(move |r| {
sender.send(ThreadEvent::from(r)); sender.send(ThreadEvent::from(r));
}))); })));
}
for (k, v) in &s.context.mailbox_hashes {
eprintln!("{:x} -> {:?}", k, v);
} }
s s
} }
@ -348,9 +344,7 @@ impl<W: Write> State<W> {
} }
/// Convert user commands to actions/method calls. /// Convert user commands to actions/method calls.
fn parse_command(&mut self, cmd: &str) { fn parse_command(&mut self, cmd: &str) {
eprintln!("cmd is {}", cmd);
let result = parse_command(&cmd.as_bytes()).to_full_result(); let result = parse_command(&cmd.as_bytes()).to_full_result();
eprintln!("rseult is {:?}", result);
if let Ok(v) = result { if let Ok(v) = result {
self.rcv_event(UIEvent { self.rcv_event(UIEvent {

View File

@ -40,8 +40,8 @@ impl File {
pub fn file(&mut self) -> std::fs::File { pub fn file(&mut self) -> std::fs::File {
std::fs::File::create(&self.path).unwrap() std::fs::File::create(&self.path).unwrap()
} }
pub fn path(&mut self) -> &mut PathBuf { pub fn path(&self) -> &PathBuf {
&mut self.path &self.path
} }
} }