mirror of
https://github.com/danbulant/dotfiles
synced 2026-09-17 13:23:51 +00:00
update
This commit is contained in:
parent
402a348601
commit
202e29c87c
5 changed files with 572 additions and 24 deletions
18
flake.lock
18
flake.lock
|
|
@ -1689,11 +1689,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs-unstable": {
|
"nixpkgs-unstable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788179007,
|
"lastModified": 1788531059,
|
||||||
"narHash": "sha256-hn1oU2rue2SYK8dAr8+WNZWtbsz1S2W5mnHlSEuh3bo=",
|
"narHash": "sha256-hLD4l3QOGBQhkVp3mQ2lJ/YbEi99qUgKapb40KovZ88=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "34ab99075ac4f7e40cf037eef32cb1c360bb85e9",
|
"rev": "801bef6abd86b91e51083066b83fb354a11fc640",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -1766,11 +1766,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs_13": {
|
"nixpkgs_13": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788179007,
|
"lastModified": 1788531059,
|
||||||
"narHash": "sha256-hn1oU2rue2SYK8dAr8+WNZWtbsz1S2W5mnHlSEuh3bo=",
|
"narHash": "sha256-hLD4l3QOGBQhkVp3mQ2lJ/YbEi99qUgKapb40KovZ88=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "34ab99075ac4f7e40cf037eef32cb1c360bb85e9",
|
"rev": "801bef6abd86b91e51083066b83fb354a11fc640",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
@ -1961,11 +1961,11 @@
|
||||||
"rust-overlay": "rust-overlay_2"
|
"rust-overlay": "rust-overlay_2"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1788263493,
|
"lastModified": 1788547023,
|
||||||
"narHash": "sha256-VCwnvc3sHbp8Ln4GugqbJHNl0CVelsxzdWMLfDaFXbw=",
|
"narHash": "sha256-xr/YlrdPgCE/DouJdKVfMhEGldzsvgXs8k5heknctVY=",
|
||||||
"owner": "can1357",
|
"owner": "can1357",
|
||||||
"repo": "oh-my-pi",
|
"repo": "oh-my-pi",
|
||||||
"rev": "45aed39ae01f1086f56983563b80c8c079df5cfd",
|
"rev": "5964a0f7649275bcde818f20073193fd032451f2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|
|
||||||
20
flake.nix
20
flake.nix
|
|
@ -168,6 +168,26 @@
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
desktopCompatibilityOverlay = _: prev: {
|
desktopCompatibilityOverlay = _: prev: {
|
||||||
|
codex =
|
||||||
|
let
|
||||||
|
version = "0.153.4";
|
||||||
|
src = prev.fetchFromGitHub {
|
||||||
|
owner = "openai";
|
||||||
|
repo = "codex";
|
||||||
|
tag = "rust-v${version}";
|
||||||
|
hash = "sha256-lHiDj5SodaM3mh8goMm6esfejeAT+Y3JJWrRnyj6sJo=";
|
||||||
|
};
|
||||||
|
sourceRoot = "${src.name}/codex-rs";
|
||||||
|
in
|
||||||
|
prev.codex.overrideAttrs (_: {
|
||||||
|
inherit version src sourceRoot;
|
||||||
|
cargoHash = "sha256-GG6kOXmCdq+bZLU2ul0DIVL8lDuweayvZvXn6+bcUZw=";
|
||||||
|
cargoDeps = prev.rustPlatform.fetchCargoVendor {
|
||||||
|
inherit src sourceRoot;
|
||||||
|
hash = "sha256-GG6kOXmCdq+bZLU2ul0DIVL8lDuweayvZvXn6+bcUZw=";
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
# Hyprland 0.56.1 requires glaze >= 7 and < 8, while this
|
# Hyprland 0.56.1 requires glaze >= 7 and < 8, while this
|
||||||
# nixpkgs revision provides glaze 8.0.0.
|
# nixpkgs revision provides glaze 8.0.0.
|
||||||
hyprland = prev.hyprland.override {
|
hyprland = prev.hyprland.override {
|
||||||
|
|
|
||||||
74
pkgs/delta-bin/package.nix
Normal file
74
pkgs/delta-bin/package.nix
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
alsa-lib,
|
||||||
|
autoPatchelfHook,
|
||||||
|
dbus,
|
||||||
|
fontconfig,
|
||||||
|
glib,
|
||||||
|
lib,
|
||||||
|
libGL,
|
||||||
|
libsecret,
|
||||||
|
makeWrapper,
|
||||||
|
openssl,
|
||||||
|
requireFile,
|
||||||
|
stdenv,
|
||||||
|
vulkan-loader,
|
||||||
|
wayland,
|
||||||
|
xkeyboard_config,
|
||||||
|
}:
|
||||||
|
|
||||||
|
stdenv.mkDerivation (finalAttrs: {
|
||||||
|
pname = "delta-bin";
|
||||||
|
version = "0.6.1";
|
||||||
|
|
||||||
|
src = requireFile {
|
||||||
|
name = "delta-linux-x86_64.tar.gz";
|
||||||
|
url = "https://delta.dev/download";
|
||||||
|
hash = "sha256-Q/CSFKlIDd47DwQk/QatfBs8AtM/ObTA3TFOTvggcsE=";
|
||||||
|
message = ''
|
||||||
|
Delta is beta-gated and cannot be downloaded without a Zed account.
|
||||||
|
Download the Linux x86_64 archive from https://delta.dev/download, then run:
|
||||||
|
|
||||||
|
nix-store --add-fixed sha256 delta-linux-x86_64.tar.gz
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
autoPatchelfHook
|
||||||
|
makeWrapper
|
||||||
|
];
|
||||||
|
runtimeDependencies = [
|
||||||
|
alsa-lib
|
||||||
|
dbus
|
||||||
|
fontconfig
|
||||||
|
glib
|
||||||
|
libGL
|
||||||
|
libsecret
|
||||||
|
openssl
|
||||||
|
vulkan-loader
|
||||||
|
wayland
|
||||||
|
];
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
runHook preInstall
|
||||||
|
|
||||||
|
mkdir -p $out
|
||||||
|
cp -a ./. $out/
|
||||||
|
rm $out/install.sh
|
||||||
|
|
||||||
|
runHook postInstall
|
||||||
|
'';
|
||||||
|
|
||||||
|
postFixup = ''
|
||||||
|
wrapProgram $out/bin/delta \
|
||||||
|
--set-default XKB_CONFIG_ROOT "${xkeyboard_config}/share/X11/xkb"
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "Collaborative agent workspace from the creators of Zed";
|
||||||
|
homepage = "https://delta.dev/";
|
||||||
|
license = lib.licenses.unfree;
|
||||||
|
mainProgram = "delta";
|
||||||
|
platforms = [ "x86_64-linux" ];
|
||||||
|
sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ];
|
||||||
|
};
|
||||||
|
})
|
||||||
452
scripts/app-usage-report.py
Executable file
452
scripts/app-usage-report.py
Executable file
|
|
@ -0,0 +1,452 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Report GUI packages declared for a Home Manager user but rarely launched.
|
||||||
|
|
||||||
|
The primary signal is DankMaterialShell's launcher history. Shell history,
|
||||||
|
notification history, and configuration references are included as secondary
|
||||||
|
signals so the report can expose likely false positives instead of silently
|
||||||
|
classifying them as unused.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import configparser
|
||||||
|
import datetime as dt
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from collections import defaultdict
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
TEXT_SUFFIXES = {
|
||||||
|
"",
|
||||||
|
".conf",
|
||||||
|
".desktop",
|
||||||
|
".fish",
|
||||||
|
".ini",
|
||||||
|
".json",
|
||||||
|
".kdl",
|
||||||
|
".lua",
|
||||||
|
".nix",
|
||||||
|
".nu",
|
||||||
|
".service",
|
||||||
|
".sh",
|
||||||
|
".toml",
|
||||||
|
".yaml",
|
||||||
|
".yml",
|
||||||
|
}
|
||||||
|
SKIP_DIRS = {
|
||||||
|
".git",
|
||||||
|
".mozilla",
|
||||||
|
"Cache",
|
||||||
|
"GPUCache",
|
||||||
|
"IndexedDB",
|
||||||
|
"Local Storage",
|
||||||
|
"Service Worker",
|
||||||
|
"cache",
|
||||||
|
"node_modules",
|
||||||
|
"storage",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Find rarely launched GUI applications declared in a Home Manager profile."
|
||||||
|
)
|
||||||
|
parser.add_argument("--flake", type=Path, default=Path(__file__).resolve().parents[1])
|
||||||
|
parser.add_argument("--host", default="fern")
|
||||||
|
parser.add_argument("--user", default=os.environ.get("USER", "dan"))
|
||||||
|
parser.add_argument(
|
||||||
|
"--days",
|
||||||
|
type=int,
|
||||||
|
default=90,
|
||||||
|
help="consider a DMS launch recent within this many days (default: 90)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--max-launches",
|
||||||
|
type=int,
|
||||||
|
default=3,
|
||||||
|
help="label an app rare when its lifetime DMS count is at most this value (default: 3)",
|
||||||
|
)
|
||||||
|
parser.add_argument("--all", action="store_true", help="include recently used applications")
|
||||||
|
parser.add_argument("--json", action="store_true", help="emit machine-readable JSON")
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def nix_packages(flake: Path, host: str, user: str) -> list[dict[str, Any]]:
|
||||||
|
installable = (
|
||||||
|
f".#nixosConfigurations.{host}.config.home-manager.users.{user}.home.packages"
|
||||||
|
)
|
||||||
|
result = subprocess.run(
|
||||||
|
["nix", "eval", "--json", installable],
|
||||||
|
cwd=flake,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
)
|
||||||
|
if result.returncode:
|
||||||
|
sys.stderr.write(result.stderr)
|
||||||
|
raise SystemExit(result.returncode)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"name": Path(out_path).name,
|
||||||
|
"pname": None,
|
||||||
|
"version": None,
|
||||||
|
"outPath": out_path,
|
||||||
|
}
|
||||||
|
for out_path in json.loads(result.stdout)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(value: str) -> str:
|
||||||
|
value = value.casefold().removesuffix(".desktop")
|
||||||
|
return re.sub(r"[^a-z0-9]+", "", value)
|
||||||
|
|
||||||
|
|
||||||
|
def executable_from(exec_line: str) -> str:
|
||||||
|
try:
|
||||||
|
words = shlex.split(exec_line)
|
||||||
|
except ValueError:
|
||||||
|
words = exec_line.split()
|
||||||
|
words = [word for word in words if not word.startswith("%")]
|
||||||
|
while words and ("=" in words[0] or words[0] in {"env", "nohup"}):
|
||||||
|
words.pop(0)
|
||||||
|
return Path(words[0]).name if words else ""
|
||||||
|
|
||||||
|
|
||||||
|
def desktop_entry(path: Path) -> dict[str, str] | None:
|
||||||
|
parser = configparser.ConfigParser(interpolation=None, strict=False)
|
||||||
|
try:
|
||||||
|
parser.read(path, encoding="utf-8")
|
||||||
|
entry = parser["Desktop Entry"]
|
||||||
|
except (OSError, UnicodeError, KeyError, configparser.Error):
|
||||||
|
return None
|
||||||
|
if entry.get("Type", "Application") != "Application":
|
||||||
|
return None
|
||||||
|
if entry.getboolean("Hidden", fallback=False) or entry.getboolean(
|
||||||
|
"NoDisplay", fallback=False
|
||||||
|
):
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"id": path.stem,
|
||||||
|
"name": entry.get("Name", path.stem),
|
||||||
|
"exec": executable_from(entry.get("Exec", "")),
|
||||||
|
"wm_class": entry.get("StartupWMClass", ""),
|
||||||
|
"desktop_file": str(path),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def package_key(store_name: str) -> str:
|
||||||
|
name = re.sub(r"^[a-z0-9]{32}-", "", store_name)
|
||||||
|
parts = name.split("-")
|
||||||
|
for index, part in enumerate(parts[1:], start=1):
|
||||||
|
if re.match(r"v?\d", part):
|
||||||
|
return "-".join(parts[:index]).casefold()
|
||||||
|
return name.casefold()
|
||||||
|
|
||||||
|
|
||||||
|
def active_gui_packages(
|
||||||
|
home: Path, user: str, declared_packages: list[dict[str, Any]]
|
||||||
|
) -> tuple[list[dict[str, Any]], int]:
|
||||||
|
profile = Path("/etc/profiles/per-user") / user
|
||||||
|
if not profile.exists():
|
||||||
|
profile = home / ".nix-profile"
|
||||||
|
applications = profile / "share/applications"
|
||||||
|
declared_keys = {package_key(package["name"]) for package in declared_packages}
|
||||||
|
grouped: dict[Path, list[dict[str, str]]] = defaultdict(list)
|
||||||
|
skipped = 0
|
||||||
|
for path in applications.glob("*.desktop"):
|
||||||
|
try:
|
||||||
|
resolved = path.resolve(strict=True)
|
||||||
|
owner = Path(*resolved.parts[:4])
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
if package_key(owner.name) not in declared_keys:
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
entry = desktop_entry(path)
|
||||||
|
if entry:
|
||||||
|
grouped[owner].append(entry)
|
||||||
|
return (
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"name": re.sub(r"^[a-z0-9]{32}-", "", owner.name),
|
||||||
|
"pname": package_key(owner.name),
|
||||||
|
"version": None,
|
||||||
|
"outPath": str(owner),
|
||||||
|
"entries": entries,
|
||||||
|
}
|
||||||
|
for owner, entries in grouped.items()
|
||||||
|
],
|
||||||
|
skipped,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_json(path: Path) -> Any:
|
||||||
|
try:
|
||||||
|
return json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, UnicodeError, json.JSONDecodeError):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def usage_index(path: Path) -> tuple[dict[str, Any], dict[str, list[str]]]:
|
||||||
|
records = load_json(path).get("appUsageRanking", {})
|
||||||
|
indexes: dict[str, list[str]] = defaultdict(list)
|
||||||
|
for key, record in records.items():
|
||||||
|
for value in (key, record.get("name", ""), executable_from(record.get("exec", ""))):
|
||||||
|
token = normalize(str(value))
|
||||||
|
if token and key not in indexes[token]:
|
||||||
|
indexes[token].append(key)
|
||||||
|
return records, indexes
|
||||||
|
|
||||||
|
|
||||||
|
def entry_tokens(entry: dict[str, str]) -> set[str]:
|
||||||
|
return {
|
||||||
|
token
|
||||||
|
for token in (
|
||||||
|
normalize(entry["id"]),
|
||||||
|
normalize(entry["name"]),
|
||||||
|
normalize(entry["exec"]),
|
||||||
|
normalize(entry["wm_class"]),
|
||||||
|
)
|
||||||
|
if len(token) >= 3
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def match_usage_keys(entry: dict[str, str], index: dict[str, list[str]]) -> set[str]:
|
||||||
|
matches: set[str] = set()
|
||||||
|
for token in entry_tokens(entry):
|
||||||
|
matches.update(index.get(token, []))
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
|
def notification_counts(path: Path) -> dict[str, int]:
|
||||||
|
data = load_json(path)
|
||||||
|
notifications = data if isinstance(data, list) else data.get("notifications", [])
|
||||||
|
counts: dict[str, int] = defaultdict(int)
|
||||||
|
for item in notifications if isinstance(notifications, list) else []:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
for field in ("desktopEntry", "appName"):
|
||||||
|
token = normalize(str(item.get(field, "")))
|
||||||
|
if token:
|
||||||
|
counts[token] += 1
|
||||||
|
return counts
|
||||||
|
|
||||||
|
|
||||||
|
def history_text(home: Path) -> str:
|
||||||
|
chunks: list[str] = []
|
||||||
|
for path in (
|
||||||
|
home / ".local/share/fish/fish_history",
|
||||||
|
home / ".config/nushell/history.txt",
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
chunks.append(path.read_text(encoding="utf-8", errors="ignore"))
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return "\n".join(chunks).casefold()
|
||||||
|
|
||||||
|
|
||||||
|
def iter_config_files(roots: Iterable[Path]) -> Iterable[Path]:
|
||||||
|
seen: set[Path] = set()
|
||||||
|
for root in roots:
|
||||||
|
if not root.is_dir():
|
||||||
|
continue
|
||||||
|
for current, dirs, files in os.walk(root):
|
||||||
|
dirs[:] = [name for name in dirs if name not in SKIP_DIRS]
|
||||||
|
for name in files:
|
||||||
|
path = Path(current) / name
|
||||||
|
if path in seen or path.suffix.casefold() not in TEXT_SUFFIXES:
|
||||||
|
continue
|
||||||
|
seen.add(path)
|
||||||
|
try:
|
||||||
|
if path.stat().st_size <= 1_000_000:
|
||||||
|
yield path
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
def config_references(
|
||||||
|
roots: Iterable[Path],
|
||||||
|
home_config: Path,
|
||||||
|
token_to_packages: dict[str, set[int]],
|
||||||
|
) -> dict[int, list[str]]:
|
||||||
|
references: dict[int, list[str]] = defaultdict(list)
|
||||||
|
searchable = sorted(
|
||||||
|
(token for token in token_to_packages if len(token) >= 4),
|
||||||
|
key=len,
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
if not searchable:
|
||||||
|
return references
|
||||||
|
pattern = re.compile(
|
||||||
|
r"(?<![A-Za-z0-9])(?:"
|
||||||
|
+ "|".join(re.escape(token) for token in searchable)
|
||||||
|
+ r")(?![A-Za-z0-9])",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
for path in iter_config_files(roots):
|
||||||
|
try:
|
||||||
|
text = path.read_text(encoding="utf-8", errors="ignore")
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
matched_packages: set[int] = set()
|
||||||
|
for match in pattern.finditer(text):
|
||||||
|
matched_packages.update(token_to_packages.get(match.group(0).casefold(), set()))
|
||||||
|
for package_id in matched_packages:
|
||||||
|
references[package_id].append(str(path))
|
||||||
|
|
||||||
|
normalized_tokens: dict[str, set[int]] = defaultdict(set)
|
||||||
|
for token, package_ids in token_to_packages.items():
|
||||||
|
normalized_tokens[normalize(token)].update(package_ids)
|
||||||
|
try:
|
||||||
|
config_children = list(home_config.iterdir())
|
||||||
|
except OSError:
|
||||||
|
config_children = []
|
||||||
|
for path in config_children:
|
||||||
|
for package_id in normalized_tokens.get(normalize(path.name), set()):
|
||||||
|
path_string = str(path)
|
||||||
|
if path_string not in references[package_id]:
|
||||||
|
references[package_id].append(path_string)
|
||||||
|
return references
|
||||||
|
|
||||||
|
|
||||||
|
def iso_time(timestamp_ms: int | float | None) -> str | None:
|
||||||
|
if not timestamp_ms:
|
||||||
|
return None
|
||||||
|
return dt.datetime.fromtimestamp(float(timestamp_ms) / 1000, dt.timezone.utc).isoformat(
|
||||||
|
timespec="seconds"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(args: argparse.Namespace) -> dict[str, Any]:
|
||||||
|
home = Path.home() if args.user == os.environ.get("USER") else Path("/home") / args.user
|
||||||
|
dms_state = home / ".local/state/DankMaterialShell/appusage.json"
|
||||||
|
records, usage_by_token = usage_index(dms_state)
|
||||||
|
notifications = notification_counts(home / ".cache/DankMaterialShell/notification_history.json")
|
||||||
|
shell_history = history_text(home)
|
||||||
|
|
||||||
|
declared_packages = nix_packages(args.flake, args.host, args.user)
|
||||||
|
gui_packages, skipped_profile_entries = active_gui_packages(
|
||||||
|
home, args.user, declared_packages
|
||||||
|
)
|
||||||
|
|
||||||
|
token_to_packages: dict[str, set[int]] = defaultdict(set)
|
||||||
|
raw_tokens: dict[int, set[str]] = defaultdict(set)
|
||||||
|
for package_id, package in enumerate(gui_packages):
|
||||||
|
for entry in package["entries"]:
|
||||||
|
for raw in (entry["id"], entry["exec"], entry["wm_class"]):
|
||||||
|
token = raw.casefold().strip()
|
||||||
|
if len(token) >= 4:
|
||||||
|
raw_tokens[package_id].add(token)
|
||||||
|
token_to_packages[token].add(package_id)
|
||||||
|
|
||||||
|
config_refs = config_references(
|
||||||
|
(args.flake / ".config",),
|
||||||
|
home / ".config",
|
||||||
|
token_to_packages,
|
||||||
|
)
|
||||||
|
cutoff = dt.datetime.now(dt.timezone.utc).timestamp() * 1000 - args.days * 86_400_000
|
||||||
|
report: list[dict[str, Any]] = []
|
||||||
|
|
||||||
|
for package_id, package in enumerate(gui_packages):
|
||||||
|
usage_keys: set[str] = set()
|
||||||
|
app_names: list[str] = []
|
||||||
|
notify_count = 0
|
||||||
|
for entry in package["entries"]:
|
||||||
|
usage_keys.update(match_usage_keys(entry, usage_by_token))
|
||||||
|
app_names.append(entry["name"])
|
||||||
|
notify_count += sum(notifications.get(token, 0) for token in entry_tokens(entry))
|
||||||
|
|
||||||
|
launches = sum(int(records[key].get("usageCount", 0)) for key in usage_keys)
|
||||||
|
last_used_ms = max(
|
||||||
|
(float(records[key].get("lastUsed", 0)) for key in usage_keys), default=0
|
||||||
|
)
|
||||||
|
history_hits = sum(shell_history.count(token) for token in raw_tokens[package_id])
|
||||||
|
recent = last_used_ms >= cutoff
|
||||||
|
if recent:
|
||||||
|
status = "recent"
|
||||||
|
elif last_used_ms:
|
||||||
|
status = (
|
||||||
|
"stale-rare"
|
||||||
|
if launches <= args.max_launches
|
||||||
|
else "stale-frequent"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
status = "configured-only" if config_refs[package_id] else "never"
|
||||||
|
|
||||||
|
report.append(
|
||||||
|
{
|
||||||
|
"package": package.get("pname") or package["name"],
|
||||||
|
"version": package.get("version"),
|
||||||
|
"applications": sorted(set(app_names)),
|
||||||
|
"status": status,
|
||||||
|
"dms_launches": launches,
|
||||||
|
"last_dms_launch": iso_time(last_used_ms),
|
||||||
|
"shell_history_hits": history_hits,
|
||||||
|
"notification_hits": notify_count,
|
||||||
|
"config_references": sorted(config_refs[package_id]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
order = {
|
||||||
|
"never": 0,
|
||||||
|
"configured-only": 1,
|
||||||
|
"stale-rare": 2,
|
||||||
|
"stale-frequent": 3,
|
||||||
|
"recent": 4,
|
||||||
|
}
|
||||||
|
report.sort(
|
||||||
|
key=lambda row: (
|
||||||
|
order[row["status"]],
|
||||||
|
row["dms_launches"],
|
||||||
|
row["last_dms_launch"] or "",
|
||||||
|
row["package"].casefold(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"host": args.host,
|
||||||
|
"user": args.user,
|
||||||
|
"window_days": args.days,
|
||||||
|
"dms_usage_file": str(dms_state),
|
||||||
|
"gui_package_count": len(gui_packages),
|
||||||
|
"unmatched_profile_entry_count": skipped_profile_entries,
|
||||||
|
"applications": report if args.all else [row for row in report if row["status"] != "recent"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def print_report(result: dict[str, Any]) -> None:
|
||||||
|
rows = result["applications"]
|
||||||
|
print(
|
||||||
|
f"{result['host']} GUI package usage: {len(rows)} shown / "
|
||||||
|
f"{result['gui_package_count']} detected "
|
||||||
|
f"({result['window_days']}-day DMS window)"
|
||||||
|
)
|
||||||
|
print("STATUS LAUNCHES LAST DMS LAUNCH HIST NOTIF CONFIG PACKAGE / APPLICATIONS")
|
||||||
|
for row in rows:
|
||||||
|
last = row["last_dms_launch"][:10] if row["last_dms_launch"] else "never"
|
||||||
|
apps = ", ".join(row["applications"])
|
||||||
|
print(
|
||||||
|
f"{row['status']:<16} {row['dms_launches']:>8} {last:<17} "
|
||||||
|
f"{row['shell_history_hits']:>5} {row['notification_hits']:>5} "
|
||||||
|
f"{len(row['config_references']):>6} {row['package']} / {apps}"
|
||||||
|
)
|
||||||
|
print("\nDMS launches are authoritative only for apps opened through the DMS launcher.")
|
||||||
|
print("HIST, NOTIF, and CONFIG are secondary evidence; inspect them before removing a package.")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args = parse_args()
|
||||||
|
result = analyze(args)
|
||||||
|
if args.json:
|
||||||
|
json.dump(result, sys.stdout, indent=2)
|
||||||
|
print()
|
||||||
|
else:
|
||||||
|
print_report(result)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -23,6 +23,7 @@
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
system = pkgs.stdenv.hostPlatform.system;
|
system = pkgs.stdenv.hostPlatform.system;
|
||||||
|
delta-bin = pkgs.callPackage ../../pkgs/delta-bin/package.nix { };
|
||||||
dmsShell = dms.packages.${system}.dms-shell.overrideAttrs (oldAttrs: {
|
dmsShell = dms.packages.${system}.dms-shell.overrideAttrs (oldAttrs: {
|
||||||
postPatch = (oldAttrs.postPatch or "") + ''
|
postPatch = (oldAttrs.postPatch or "") + ''
|
||||||
if [[ -f quickshell/Services/PopoutService.qml ]]; then
|
if [[ -f quickshell/Services/PopoutService.qml ]]; then
|
||||||
|
|
@ -142,6 +143,8 @@ in
|
||||||
stateVersion = "25.11";
|
stateVersion = "25.11";
|
||||||
|
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
|
delta-bin
|
||||||
|
kicad
|
||||||
protontricks
|
protontricks
|
||||||
waypipe
|
waypipe
|
||||||
cinny
|
cinny
|
||||||
|
|
@ -157,7 +160,7 @@ in
|
||||||
wine
|
wine
|
||||||
codexbar.packages.${pkgs.system}.default
|
codexbar.packages.${pkgs.system}.default
|
||||||
codex
|
codex
|
||||||
jellyfin-desktop
|
#jellyfin-desktop
|
||||||
(kdePackages.qt6ct.overrideAttrs (oldAttrs: {
|
(kdePackages.qt6ct.overrideAttrs (oldAttrs: {
|
||||||
patches = (oldAttrs.patches or [ ]) ++ [ ../../pkgs/qt6ct-0.11.patch ];
|
patches = (oldAttrs.patches or [ ]) ++ [ ../../pkgs/qt6ct-0.11.patch ];
|
||||||
name = "qt6ct-kde";
|
name = "qt6ct-kde";
|
||||||
|
|
@ -176,12 +179,12 @@ in
|
||||||
nixd
|
nixd
|
||||||
buck2
|
buck2
|
||||||
(rusic.packages.${system}.default)
|
(rusic.packages.${system}.default)
|
||||||
affine
|
#affine
|
||||||
voxtype-vulkan
|
voxtype-vulkan
|
||||||
#affinity-nix.packages.x86_64-linux.v3
|
#affinity-nix.packages.x86_64-linux.v3
|
||||||
biome
|
biome
|
||||||
bun
|
bun
|
||||||
lenovo-legion
|
#lenovo-legion
|
||||||
itch
|
itch
|
||||||
filezilla
|
filezilla
|
||||||
nicotine-plus
|
nicotine-plus
|
||||||
|
|
@ -203,11 +206,11 @@ in
|
||||||
libxkbfile
|
libxkbfile
|
||||||
|
|
||||||
# dioxus-cli
|
# dioxus-cli
|
||||||
cosmic-files
|
#cosmic-files
|
||||||
cosmic-player
|
#cosmic-player
|
||||||
cosmic-screenshot
|
#cosmic-screenshot
|
||||||
cosmic-applibrary
|
#cosmic-applibrary
|
||||||
cosmic-ext-calculator
|
#cosmic-ext-calculator
|
||||||
cosmic-icons
|
cosmic-icons
|
||||||
examine
|
examine
|
||||||
|
|
||||||
|
|
@ -227,7 +230,7 @@ in
|
||||||
usbimager
|
usbimager
|
||||||
#bitwarden-desktop
|
#bitwarden-desktop
|
||||||
#metasploit
|
#metasploit
|
||||||
lenovo-legion
|
#lenovo-legion
|
||||||
burpsuite
|
burpsuite
|
||||||
zap
|
zap
|
||||||
kubernetes-helm
|
kubernetes-helm
|
||||||
|
|
@ -268,17 +271,16 @@ in
|
||||||
lf
|
lf
|
||||||
rawtherapee
|
rawtherapee
|
||||||
syncthingtray
|
syncthingtray
|
||||||
anki-bin
|
#anki-bin
|
||||||
xournalpp
|
xournalpp
|
||||||
simple-scan
|
simple-scan
|
||||||
godot_4
|
godot_4
|
||||||
#rar
|
#rar
|
||||||
wootility
|
#wootility
|
||||||
#surrealdb
|
#surrealdb
|
||||||
pico-sdk
|
pico-sdk
|
||||||
elf2uf2-rs
|
elf2uf2-rs
|
||||||
obsidian
|
obsidian
|
||||||
betaflight-configurator
|
|
||||||
home-manager
|
home-manager
|
||||||
#glxinfo
|
#glxinfo
|
||||||
pciutils
|
pciutils
|
||||||
|
|
@ -318,8 +320,8 @@ in
|
||||||
datovka
|
datovka
|
||||||
nwg-displays
|
nwg-displays
|
||||||
wireguard-tools
|
wireguard-tools
|
||||||
mongodb-compass
|
#mongodb-compass
|
||||||
unstable.mongodb-tools
|
#unstable.mongodb-tools
|
||||||
#hashcat
|
#hashcat
|
||||||
tldr
|
tldr
|
||||||
#dunst
|
#dunst
|
||||||
|
|
@ -430,7 +432,7 @@ in
|
||||||
graphviz
|
graphviz
|
||||||
|
|
||||||
#blender
|
#blender
|
||||||
warpinator
|
#warpinator
|
||||||
|
|
||||||
awatcher
|
awatcher
|
||||||
tigervnc
|
tigervnc
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue