aboutsummaryrefslogtreecommitdiffstats
path: root/pkgs/by-name/ts/tskm/src/interface
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-04-04 16:07:34 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2025-04-04 16:07:34 +0200
commit2a9d37026a5b2599930c2d7338cdf04bf254812b (patch)
treea818c19634a05f500daaccf42e36205a28a52635 /pkgs/by-name/ts/tskm/src/interface
parentfix(pkgs/tskm): Remove typos (diff)
downloadnixos-config-2a9d37026a5b2599930c2d7338cdf04bf254812b.zip
feat(pkgs/tskm): Support listing the open tabs in a project
Diffstat (limited to 'pkgs/by-name/ts/tskm/src/interface')
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/open/handle.rs43
-rw-r--r--pkgs/by-name/ts/tskm/src/interface/open/mod.rs104
2 files changed, 147 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
index 5738d232..08bd1de4 100644
--- a/pkgs/by-name/ts/tskm/src/interface/open/handle.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/open/handle.rs
@@ -58,6 +58,49 @@ pub fn handle(command: OpenCommand) -> Result<()> {
open_in_browser(&selected_project).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(())
}
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 a70793bc..2dc75957 100644
--- a/pkgs/by-name/ts/tskm/src/interface/open/mod.rs
+++ b/pkgs/by-name/ts/tskm/src/interface/open/mod.rs
@@ -1,2 +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 {}