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 | 232 | ||||
-rw-r--r-- | pkgs/by-name/ts/tskm/src/interface/open/mod.rs | 106 |
2 files changed, 338 insertions, 0 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 new file mode 100644 index 00000000..4d7341b2 --- /dev/null +++ b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs @@ -0,0 +1,232 @@ +use std::{ + fs, + net::{IpAddr, Ipv4Addr}, + path::PathBuf, + process, +}; + +use anyhow::{bail, Context, Result}; +use log::{error, info, warn}; +use url::Url; + +use crate::{cli::OpenCommand, rofi, state::State, task}; + +pub fn handle(command: OpenCommand, state: &mut State) -> Result<()> { + match command { + OpenCommand::Review => { + for project in task::Project::all().context("Failed to get all project files")? { + if project.is_touched() { + info!("Reviewing project: '{}'", project.to_project_display()); + 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 ('{}')", + project.to_project_display() + ) + })?; + } + } + } + OpenCommand::Project { project, url } => { + project.touch().context("Failed to touch project")?; + open_in_browser(&project, state, url).with_context(|| { + format!("Failed to open project: {}", project.to_project_display()) + })?; + } + OpenCommand::Select { url } => { + let selected_project: task::Project = task::Project::from_project_string( + &rofi::select( + task::Project::all() + .context("Failed to get all registered projects")? + .iter() + .map(task::Project::to_project_display) + .collect::<Vec<_>>() + .as_slice(), + ) + .context("Failed to get selected project")?, + ) + .expect("This should work, as we send only projects in"); + + selected_project + .touch() + .context("Failed to touch project")?; + + 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!"); + }; + + 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 { + " " + } + }; + 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"); + + ( + ip.parse().expect("Should be a valid ip address"), + pid.parse().expect("Should be a valid pid"), + ) + }; + + 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. + } + } 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}"))?; + } + + 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")?; + } + + Ok(()) +} diff --git a/pkgs/by-name/ts/tskm/src/interface/open/mod.rs b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs new file mode 100644 index 00000000..2dc75957 --- /dev/null +++ b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs @@ -0,0 +1,106 @@ +use std::{collections::HashMap, fs::File, io}; + +use anyhow::{Context, Result}; +use lz4_flex::decompress_size_prepended; +use serde::Deserialize; +use serde_json::Value; +use url::Url; + +use crate::task::Project; + +pub mod handle; +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") + .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"; + + let mut buf = [0u8; 8]; + file.read_exact(&mut buf) + .context("Failed to read the mozlz40 header.")?; + + assert_eq!(buf, MOZLZ4_MAGIC_NUMBER); + + let mut buf = vec![]; + file.read_to_end(&mut buf).context("Failed to read file")?; + + let uncompressed = decompress_size_prepended(&buf).context("Failed to decompress file")?; + + Ok(String::from_utf8(uncompressed).expect("This should be valid json and thus utf8")) +} + +#[derive(Deserialize, Debug)] +pub struct SessionStore { + pub windows: Vec<Window>, +} + +#[derive(Deserialize, Debug)] +pub struct Window { + pub tabs: Vec<Tab>, + pub selected: usize, +} + +#[derive(Deserialize, 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>, +} + +#[derive(Deserialize, Debug)] +pub struct TabEntry { + pub url: Url, + 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, +} + +#[derive(Deserialize, Debug, Clone, Copy)] +pub struct TabAttributes {} |