diff options
Diffstat (limited to 'pkgs/by-name/ts/tskm/src/interface/open')
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/open/handle.rs | 294 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/open/mod.rs | 212 |
2 files changed, 250 insertions, 256 deletions
diff --git a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs index 82f468b3..ca54b422 100644 --- a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs +++ b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs @@ -8,37 +8,46 @@ // You should have received a copy of the License along with this program. // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. -use std::{ - fs, - net::{IpAddr, Ipv4Addr}, - path::PathBuf, - process, -}; - -use anyhow::{Context, Result, bail}; -use log::{error, info, warn}; +use anyhow::{bail, Context, Result}; +use log::{error, info}; use url::Url; -use crate::{cli::OpenCommand, rofi, state::State, task}; +use crate::{browser::open_in_browser, cli::OpenCommand, rofi, state::State, task}; +fn is_empty(project: &task::Project) -> Result<bool> { + let tabs = get_tabs(project)?; + + Ok(tabs.is_empty()) +} + +#[allow(clippy::too_many_lines)] pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> { match command { - OpenCommand::Review => { + OpenCommand::Review { non_empty } => { for project in task::Project::all().context("Failed to get all project files")? { - if project.is_touched() { - info!("Reviewing project: '{}'", project.to_project_display()); + let is_empty = is_empty(project)?; + + if project.is_touched() || (non_empty && !is_empty) { + info!( + "Reviewing project: '{}' ({})", + project.to_project_display(), + if is_empty { "is empty" } else { "is not empty" } + ); open_in_browser(project, state, None).with_context(|| { format!( - "Failed to open project ('{}') in Firefox", - project.to_project_display() - ) - })?; - project.untouch().with_context(|| { - format!( - "Failed to untouch project ('{}')", + "Failed to open project ('{}') in qutebrowser", project.to_project_display() ) })?; + + if project.is_touched() { + project.untouch().with_context(|| { + format!( + "Failed to untouch project ('{}')", + project.to_project_display() + ) + })?; + } } } } @@ -68,175 +77,104 @@ pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> { open_in_browser(&selected_project, state, url).context("Failed to open project")?; } - OpenCommand::ListTabs { project } => { - let project = if let Some(p) = project { - p - } else if let Some(p) = - task::Project::get_current().context("Failed to get currently focused project")? - { - p - } else { - bail!("You need to either supply a project or have a project active!"); + OpenCommand::ListTabs { projects, mode } => { + let projects = { + if let Some(p) = projects { + p + } else if mode.is_some() { + task::Project::all() + .context("Failed to get all projects")? + .to_owned() + } else if let Some(p) = task::Project::get_current() + .context("Failed to get currently focused project")? + { + vec![p] + } else { + bail!("You need to either select projects or pass --mode"); + } }; - let session_store = project.get_sessionstore().with_context(|| { - format!( - "Failed to get session store for project: '{}'", - project.to_project_display() - ) - })?; - - let selected = session_store - .windows - .iter() - .map(|w| w.selected) - .collect::<Vec<_>>(); - - let tabs = session_store - .windows - .iter() - .flat_map(|window| window.tabs.iter()) - .map(|tab| tab.entries.get(tab.index - 1).expect("This should be Some")) - .collect::<Vec<_>>(); - - for (index, entry) in tabs.iter().enumerate() { - let index = index + 1; - let is_selected = { - if selected.contains(&index) { - "🔻 " - } else { - " " + for project in &projects { + if let Some(mode) = mode { + match mode { + crate::cli::ListMode::Empty => { + if !is_empty(project)? { + continue; + } + + // We do not need to print, tabs they are always empty. + if projects.len() > 1 { + println!("/* {} */", project.to_project_display()); + } + continue; + } + crate::cli::ListMode::NonEmpty => { + if is_empty(project)? { + continue; + } + } } - }; - println!("{}{}", is_selected, entry.url); - } - } - } - Ok(()) -} - -fn open_in_browser( - selected_project: &task::Project, - state: &mut State, - url: Option<Url>, -) -> Result<()> { - let old_project: Option<task::Project> = - task::Project::get_current().context("Failed to get currently active project")?; - let old_task: Option<task::Task> = - task::Task::get_current(state).context("Failed to get currently active task")?; - - selected_project.activate().with_context(|| { - format!( - "Failed to active project: '{}'", - selected_project.to_project_display() - ) - })?; - - let tracking_task = { - let all_tasks = selected_project.get_tasks(state).with_context(|| { - format!( - "Failed to get assoctiated tasks for project: '{}'", - selected_project.to_project_display() - ) - })?; - - let tracking_task = all_tasks.into_iter().find(|t| { - let maybe_desc = t.description(state); - if let Ok(desc) = maybe_desc { - desc == "tracking" - } else { - error!( - "Getting task description returned error: {}", - maybe_desc.expect_err("We already check for Ok") - ); - false - } - }); - - if let Some(task) = tracking_task { - info!( - "Starting task {} -> tracking", - selected_project.to_project_display() - ); - task.start(state) - .with_context(|| format!("Failed to start task {task}"))?; - } - tracking_task - }; - - let status = { - let mut args = vec!["-P".to_owned(), selected_project.to_project_display()]; - if let Some(url) = url { - args.push(url.to_string()); - } else { - let lock_file = dirs::home_dir() - .expect("Exists") - .join(".mozilla/firefox") - .join(selected_project.to_project_display()) - .join("lock"); - - if lock_file.exists() { - let (ip, pid): (IpAddr, u32) = { - let link = fs::read_link(&lock_file).with_context(|| { - format!("Failed to readlink lock at '{}'", lock_file.display()) - })?; + } - let (ip, pid) = link - .to_str() - .expect("Should work") - .split_once(':') - .expect("The split works"); + if projects.len() > 1 { + println!("/* {} */", project.to_project_display()); + } - ( - ip.parse().expect("Should be a valid ip address"), - pid.parse().expect("Should be a valid pid"), - ) + let tabs = match get_tabs(project) { + Ok(ok) => ok, + Err(err) => { + if projects.len() > 1 { + error!( + "While trying to get the sessionstore for {}: {:?}", + project.to_project_display(), + err + ); + continue; + } + + return Err(err).with_context(|| { + format!( + "While trying to get the sessionstore for {}", + project.to_project_display() + ) + }); + } }; - if ip != Ipv4Addr::new(127, 0, 0, 2) { - warn!("Your ip is weird.."); - } - - if PathBuf::from("/proc").join(pid.to_string()).exists() { - // Another Firefox instance has already been started for this project - // Add a buffer URL to force Firefox to open it in the already open instance - args.push("about:newtab".to_owned()); - } else { - // This project does not yet have another Firefox instance - // We do not need to add anything to the arguments, Firefox will open a new - // instance. + for (active, url) in tabs { + let is_selected = { + if active { + "🔻 " + } else { + " " + } + }; + println!("{is_selected}{url}"); } - } else { - // There is no lock file and thus no instance already open. } - }; - - process::Command::new("firefox") - .args(args) - .status() - .context("Failed to start firefox")? - }; - - if !status.success() { - error!("Firefox run exited with error."); + } } - if let Some(task) = tracking_task { - task.stop(state) - .with_context(|| format!("Failed to stop task {task}"))?; - } - if let Some(task) = old_task { - task.start(state) - .with_context(|| format!("Failed to start task {task}"))?; - } + Ok(()) +} - if let Some(project) = old_project { - project.activate().with_context(|| { - format!("Failed to active project {}", project.to_project_display()) - })?; - } else { - task::Project::clear().context("Failed to clear currently focused project")?; - } +fn get_tabs(project: &task::Project) -> Result<Vec<(bool, Url)>> { + let session_store = project.get_sessionstore()?; - Ok(()) + let tabs = session_store + .windows + .iter() + .flat_map(|window| window.tabs.iter()) + .filter_map(|tab| { + tab.history + .iter() + .find(|hist| hist.active) + .map(|hist| (tab.active, hist)) + }) + .collect::<Vec<_>>(); + + Ok(tabs + .into_iter() + .map(|(active, hist)| (active, hist.url.clone())) + .collect()) } diff --git a/pkgs/by-name/ts/tskm/src/interface/open/mod.rs b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs index a4060fa3..40e057c1 100644 --- a/pkgs/by-name/ts/tskm/src/interface/open/mod.rs +++ b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs @@ -8,13 +8,12 @@ // You should have received a copy of the License along with this program. // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>. -use std::{collections::HashMap, fs::File, io}; +use std::{fs::File, io::Read, str::FromStr}; -use anyhow::{Context, Result}; -use lz4_flex::decompress_size_prepended; -use serde::Deserialize; -use serde_json::Value; +use anyhow::{anyhow, Context, Result}; +use taskchampion::chrono::NaiveDateTime; use url::Url; +use yaml_rust2::Yaml; use crate::task::Project; @@ -23,94 +22,151 @@ pub use handle::handle; impl Project { pub(super) fn get_sessionstore(&self) -> Result<SessionStore> { - let path = dirs::home_dir() - .expect("Will exist") - .join(".mozilla/firefox") + let path = dirs::data_local_dir() + .context("Failed to get data dir")? + .join("qutebrowser") .join(self.to_project_display()) - .join("sessionstore-backups/recovery.jsonlz4"); - let file = decompress_mozlz4( - File::open(&path) - .with_context(|| format!("Failed to open path '{}'", path.display()))?, - ) - .with_context(|| format!("Failed to decompress file as mozlzh '{}'", path.display()))?; - - let contents: SessionStore = serde_json::from_str(&file).with_context(|| { - format!( - "Failed to deserialize file ('{}') as session store.", - path.display() - ) - })?; - Ok(contents) - } -} - -fn decompress_mozlz4<P: io::Read>(mut file: P) -> Result<String> { - const MOZLZ4_MAGIC_NUMBER: &[u8] = b"mozLz40\0"; + // NOTE(@bpeetz): We could use another real session name, but this file should + // always exist. <2025-06-03> + .join("data/sessions/_autosave.yml"); - let mut buf = [0u8; 8]; - file.read_exact(&mut buf) - .context("Failed to read the mozlz40 header.")?; + let mut file = File::open(&path) + .with_context(|| format!("Failed to open path '{}'", path.display()))?; - assert_eq!(buf, MOZLZ4_MAGIC_NUMBER); + let mut yaml_str = String::new(); + file.read_to_string(&mut yaml_str) + .context("Failed to read _autosave.yml path")?; + let yaml = yaml_rust2::YamlLoader::load_from_str(&yaml_str)?; - let mut buf = vec![]; - file.read_to_end(&mut buf).context("Failed to read file")?; + let store = qute_store_from_yaml(&yaml).context("Failed to read yaml store")?; - let uncompressed = decompress_size_prepended(&buf).context("Failed to decompress file")?; + Ok(store) + } +} - Ok(String::from_utf8(uncompressed).expect("This should be valid json and thus utf8")) +fn qute_store_from_yaml(yaml: &[Yaml]) -> Result<SessionStore> { + assert_eq!(yaml.len(), 1); + let doc = &yaml[0]; + + let hash = doc.as_hash().context("Invalid yaml")?; + let windows = hash + .get(&Yaml::String("windows".to_owned())) + .ok_or(anyhow!("Missing windows"))? + .as_vec() + .ok_or(anyhow!("Windows not vector"))?; + + Ok(SessionStore { + windows: windows + .iter() + .map(|window| { + let hash = window.as_hash().ok_or(anyhow!("Windows not hashmap"))?; + + Ok::<_, anyhow::Error>(Window { + geometry: hash + .get(&Yaml::String("geometry".to_owned())) + .ok_or(anyhow!("Missing window geometry"))? + .as_str() + .ok_or(anyhow!("geometry not string"))? + .to_owned(), + tabs: hash + .get(&Yaml::String("tabs".to_owned())) + .ok_or(anyhow!("Missing window tabs"))? + .as_vec() + .ok_or(anyhow!("Tabs not vec"))? + .iter() + .map(|tab| { + let hash = tab.as_hash().ok_or(anyhow!("Tab not hashmap"))?; + + Ok::<_, anyhow::Error>(Tab { + history: hash + .get(&Yaml::String("history".to_owned())) + .ok_or(anyhow!("Missing tab history"))? + .as_vec() + .ok_or(anyhow!("tab history not vec"))? + .iter() + .map(|history| { + let hash = history + .as_hash() + .ok_or(anyhow!("Tab history not hashmap"))?; + + Ok::<_, anyhow::Error>(TabHistory { + active: hash + .get(&Yaml::String("active".to_owned())) + .ok_or(anyhow!("Missing tab history active"))? + .as_bool() + .ok_or(anyhow!("tab history active not bool"))?, + last_visited: NaiveDateTime::from_str( + hash.get(&Yaml::String("last_visited".to_owned())) + .ok_or(anyhow!( + "Missing tab history last_visited" + ))? + .as_str() + .ok_or(anyhow!( + "tab history last_visited not string" + ))?, + ) + .context("Failed to parse last_visited")?, + pinned: hash + .get(&Yaml::String("pinned".to_owned())) + .ok_or(anyhow!("Missing tab history pinned"))? + .as_bool() + .ok_or(anyhow!("tab history pinned not bool"))?, + title: hash + .get(&Yaml::String("title".to_owned())) + .ok_or(anyhow!("Missing tab history title"))? + .as_str() + .ok_or(anyhow!("tab history title not string"))? + .to_owned(), + url: Url::parse( + hash.get(&Yaml::String("url".to_owned())) + .ok_or(anyhow!("Missing tab history url"))? + .as_str() + .ok_or(anyhow!("tab history url not string"))?, + ) + .context("Failed to parse url")?, + zoom: hash + .get(&Yaml::String("zoom".to_owned())) + .ok_or(anyhow!("Missing tab history zoom"))? + .as_f64() + .ok_or(anyhow!("tab history zoom not 64"))?, + }) + }) + .collect::<Result<Vec<_>, _>>()?, + active: hash + .get(&Yaml::String("active".to_owned())) + .unwrap_or(&Yaml::Boolean(false)) + .as_bool() + .ok_or(anyhow!("active not bool"))?, + }) + }) + .collect::<Result<Vec<_>, _>>()?, + }) + }) + .collect::<Result<Vec<_>, _>>()?, + }) } -#[derive(Deserialize, Debug)] +#[derive(Debug)] pub struct SessionStore { pub windows: Vec<Window>, } - -#[derive(Deserialize, Debug)] +#[derive(Debug)] pub struct Window { + pub geometry: String, pub tabs: Vec<Tab>, - pub selected: usize, } - -#[derive(Deserialize, Debug)] +#[derive(Debug)] pub struct Tab { - pub entries: Vec<TabEntry>, - #[serde(rename = "lastAccessed")] - pub last_accessed: u64, - pub hidden: bool, - #[serde(rename = "searchMode")] - pub search_mode: Option<Value>, - #[serde(rename = "userContextId")] - pub user_context_id: u32, - pub attributes: TabAttributes, - #[serde(rename = "extData")] - pub ext_data: Option<HashMap<String, Value>>, - pub index: usize, - #[serde(rename = "requestedIndex")] - pub requested_index: Option<u32>, - pub image: Option<Url>, + pub history: Vec<TabHistory>, + pub active: bool, } - -#[derive(Deserialize, Debug)] -pub struct TabEntry { - pub url: Url, +#[derive(Debug)] +pub struct TabHistory { + pub active: bool, + pub last_visited: NaiveDateTime, + pub pinned: bool, + // pub scroll-pos: pub title: String, - #[serde(rename = "cacheKey")] - pub cache_key: u32, - #[serde(rename = "ID")] - pub id: u32, - #[serde(rename = "docshellUUID")] - pub docshell_uuid: Value, - #[serde(rename = "resultPrincipalURI")] - pub result_principal_uri: Option<Url>, - #[serde(rename = "hasUserInteraction")] - pub has_user_interaction: bool, - #[serde(rename = "triggeringPrincipal_base64")] - pub triggering_principal_base64: Value, - #[serde(rename = "docIdentifier")] - pub doc_identifier: u32, - pub persist: bool, + pub url: Url, + pub zoom: f64, } - -#[derive(Deserialize, Debug, Clone, Copy)] -pub struct TabAttributes {} |