1mod util;
2
3use std::{env::temp_dir, fs::File, io::Write, process::Command};
4
5use lz4_flex::block::DecompressError;
6use util::{decompress_lz4, list_recovery_files};
7
8#[derive(thiserror::Error, Debug)]
10pub enum Error {
11 #[error("Firefox data dir not found: {0}")]
13 FFDirNotFound(&'static str),
14 #[error("I/O error: {0}")]
16 Io(#[from] std::io::Error),
17 #[error("LZ4 error: {0}")]
19 Lz4Decompression(#[from] DecompressError),
20 #[error("JSON error: {0}")]
22 Json(#[from] serde_json::Error),
23 #[error("Multiple errors: {0}")]
25 Multi(String),
26 #[error("Subcommand failed")]
27 ExitStatus,
28}
29
30pub type FFResult<T> = Result<T, Error>;
32
33mod recovery {
34 use serde::Deserialize;
35
36 #[derive(Deserialize, Debug)]
37 pub struct TopLevel {
38 pub windows: Vec<Window>,
39 }
40
41 #[derive(Deserialize, Debug)]
42 pub struct Window {
43 pub tabs: Vec<Tab>,
44 }
45
46 #[derive(Deserialize, Debug)]
47 pub struct Tab {
48 entries: Vec<Entry>,
49 index: usize,
50 pub image: Option<String>,
51 }
52
53 impl Tab {
54 pub fn into_entry(mut self) -> Entry {
55 self.entries.swap_remove(self.index - 1)
56 }
57 }
58
59 #[derive(Deserialize, Debug)]
60 pub struct Entry {
61 pub title: String,
62 pub url: String,
63 }
64}
65
66#[derive(Debug)]
68pub struct Tab {
69 pub title: String,
71 pub url: String,
73 pub icon: Option<String>,
75}
76
77impl From<recovery::Tab> for Tab {
78 fn from(mut t: recovery::Tab) -> Self {
79 let icon = t.image.take();
80 let recovery::Entry { title, url } = t.into_entry();
81 Tab { title, url, icon }
82 }
83}
84
85impl Tab {
86 pub fn focus(&self) -> FFResult<()> {
90 let hack = format!(
91 "<!DOCTYPE html><body>\
92 <script>window.focusTab({{url:'{}'}});\
93 open(location, '_self').close();\
94 </script></body></html>",
95 self.url
96 );
97 let path = temp_dir().join("firefox-rs-focus.html");
98 let mut f = File::create(&path)?;
99 f.write_all(hack.as_bytes())?;
100 let mut child = Command::new("firefox").arg(path).spawn()?;
101 if !child.wait()?.success() {
102 return Err(Error::ExitStatus);
103 }
104 Ok(())
105 }
106}
107
108pub fn list_tabs() -> FFResult<Vec<Tab>> {
110 let mut errors = Vec::with_capacity(0);
111 macro_rules! try_add {
113 ($result:expr) => {
114 match $result {
115 Ok(ok) => ok,
116 Err(e) => {
117 errors.push(Error::from(e));
118 continue;
119 }
120 }
121 };
122 }
123 for path_res in list_recovery_files()? {
124 let path = path_res?;
125
126 let buf = try_add!(decompress_lz4(path));
129 let topl: recovery::TopLevel = try_add!(serde_json::from_slice(&buf));
130
131 let tabs = topl
134 .windows
135 .into_iter()
136 .flat_map(|window| window.tabs)
137 .map(Tab::from)
138 .collect();
139 return Ok(tabs);
140 }
141
142 match errors.len() {
144 0 => Err(Error::FFDirNotFound("recovery.json*")),
145 1 => Err(errors.swap_remove(0)),
146 _ => Err(Error::Multi({
147 let mut errors_s = String::new();
148 for (i, e) in errors.into_iter().enumerate() {
149 errors_s += &format!("({i}) {e} ");
150 }
151 errors_s
152 })),
153 }
154}
155
156#[cfg(test)]
157mod tests {}