diff --git a/scripts/audit.py b/scripts/audit.py new file mode 100755 index 000000000..a2827f36b --- /dev/null +++ b/scripts/audit.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 + +import os, csv, argparse + +ignoreList = [] + +parser = argparse.ArgumentParser( + prog = "audit", + description = "Audit a FreeDesktop.org compatible icon set against a specification" +) +parser.add_argument( + "themePath", +) +parser.add_argument( + "-s", + "--specification", + dest="specf" +) +parser.add_argument( + "-v", + "--verbose", + action="store_true" +) +parser.add_argument( + "-r", + "--report-name", + dest="reportf", + default="report" +) +args = parser.parse_args() + +# make hrules pretty +try: + viewWidth = os.get_terminal_size().columns +except: + viewWidth = 80 + +# Check for optional dependencies to enable drawing the spec from the web. +# Otherwise fall back to included file +try: + import requests + from bs4 import BeautifulSoup +except: + print("Couldn't find bs4 and requests dependencies, falling back to using spec file...") + foundDeps = False +else: + print("Found bs4 and requests dependencies, pulling spec from the web") + foundDeps = True + +def get_soup(url: str) -> BeautifulSoup: + page = requests.get(url) + soup = BeautifulSoup(page.text, "html.parser") + return soup + +# This is kinda not great but it works +def parse_soup(soup: BeautifulSoup) -> list: + result = [] + + tables = soup.find_all("table") + #remove the context description table, we don't care about it here + tables.pop(0) + for table in tables: + rows = table.find_all("tr") + for row in rows: + text = row.contents + name = text[0].text.strip().replace("\n", "") + # skip the header row + if name == "Name": continue + result.append(name) + + return result + +def get_iso_3166() -> list: + result = [] + page = requests.get("https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2") + soup = BeautifulSoup(page.text, "html.parser") + table = soup.find_all("table", class_="wikitable")[4].tbody + rows = table.find_all("tr") + for row in rows: + code = row.text.strip().replace("\n", " ").split()[0] + if code == "Code": + continue + result.append(f"flag-{code}") + + return result + + + +def padList(iterable: list, length: int) -> list: + extensionLength = max(length - len(iterable), 0) + if args.verbose: print(f"Extending list by {extensionLength} elements") + for _ in range(extensionLength): + iterable.append(None) + + return iterable + +def populateFromDir(dir: os.DirEntry) -> set: + # This function is called recursively to populate a list + tree = [] + + for entry in os.scandir(dir.path): + if entry.name not in ignoreList: + if entry.is_dir(): + if args.verbose: print(f"Found dir at {entry.path}, going deeper") + # A spoonful of recursion to help the medicine go down + tree.extend(populateFromDir(entry)) + else: + if args.verbose: print(f"Adding file {entry.path}, named {entry.name} to SubTree") + tree.append(entry.name.removesuffix(".svg")) + + if args.verbose: print(f"The SubTree for Dir {dir.name} is {tree}") + return list(set(tree)) + +# Load a text stream of the spec into a dictionary, where KEY=icon_name & +# VALUE=a bool denoting whether the file was found in the file tree +# Iterate over the list, checking to see if the specified file can be found. +specification = {} + +if foundDeps: + url = "https://specifications.freedesktop.org/icon-naming-spec/latest/" + soup = get_soup(url) + specList = parse_soup(soup) + specList.extend(get_iso_3166()) + specList.remove("flag-aa") + specification = specification.fromkeys(specList, False) +else: + with open(args.specf, "r") as file: + print(f"Loading specification in file: {specf}...") + for line in file: + if line.startswith(("//", " ", "\n")): + continue + specification.setdefault(line.removesuffix("\n"), False) + +print("Successfully loaded specfication!") +symbolicSpecification = specification.copy() + +# Change the working dir to our target dir, to make it easier to traverse the +# tree. Try to find the file `.auditignore` in the dir root and load it into +# a list. Anything in this list doesn't exist as far as this script is +# concerned. +try: + targetDir = args.themePath +except: + print("Please specify a directory") + exit() +else: + # Save the cwd so we can switch back later + scriptDir = os.getcwd() + dir = os.fspath(targetDir) + os.chdir(dir) + +try: + with open(".auditignore", "r") as file: + print("Found .auditignore file!") + for line in file: + ignoreList.append(line.removesuffix("\n")) +except: + print("No .auditignore file found in root") +else: + print("Loaded .auditignore file!") + if args.verbose: print(f"Values contained: {ignoreList}") + +print('-' * viewWidth) + +# Generate the contents of the relevant directory tree +contents = [] + +for entry in os.scandir(): + if entry.name not in ignoreList: + if entry.is_dir(): + if args.verbose: print(f"Found dir at {entry.path}") + subDirTree = populateFromDir(entry) + contents.extend(subDirTree) + else: + if args.verbose: print(f"Adding file {entry.path} to tree") + if entry.name.endswith(".svg"): + contents.append(entry.name.removesuffix(".svg")) + +# remove duplicate names from the list +contents = list(set(contents)) + +if args.verbose: + print(f"The full dirTree is {contents}") + print('-' * viewWidth) + +# Traverse the dir tree, checking whether the found file is in the spec list. +# This is so we don't have to traverse the entire dir tree of the set, just the +# specification list which is likely to be much smaller. + +for entry in specification.keys(): + if entry in contents: + print(f"Found {entry}") + specification |= {entry: True} + else: + print(f"[!!] {entry} is missing!") + + +# Calculate percent spec coverage +totalEntries = len(specification.keys()) +existantColorEntries = list(specification.values()).count(True) + +print(f"{existantColorEntries / totalEntries * 100:.2f}% coverage of FD.o specification, color entries") + +print('-' * viewWidth) + +# Check whether things are included in symbolic entries +for entry in symbolicSpecification.keys(): + extendedEntry = entry + "-symbolic" + if extendedEntry in contents: + print(f"Found {extendedEntry}") + symbolicSpecification |= {entry: True} + else: + print(f"[!!] {extendedEntry} is missing!") + +existantSymbolicEntries = list(symbolicSpecification.values()).count(True) + +print(f"{existantSymbolicEntries / totalEntries * 100:.2f}% coverage of FD.o specification, symbolic entries") + +print('-' * viewWidth) + +# Merging results and comparing all entries +colorResults = list(specification.values()) +symbolicResults = list(symbolicSpecification.values()) +results = [] +for i, value in enumerate(colorResults): + results.append(symbolicResults[i] | value) + +existantEntries = results.count(True) + +print(f"{existantEntries / totalEntries * 100:.2f}% coverage of FD.o specification, all entries") + + +print('-' * viewWidth) +os.chdir(scriptDir) + +# Write report file with Found, Missing, and Out-of-Spec information +specList = list(specification.keys()) +foundEntries = [] +missingEntries = [] +outOfSpecEntries = [] + +# We only look at the color entry results, as symbolic is not officially in the spec +for i, result in enumerate(colorResults): + # If the result is true, then it was found + # If the result is false, then it is missing + name = specList[i] + if result: + if args.verbose: print(f"Adding {name} to Found...") + foundEntries.append(name) + else: + if args.verbose: print(f"Adding {name} to Missing...") + missingEntries.append(name) + +# Now we want to go through all of the entries we found in the initial contents and +# point out those that aren't in the spec +for entry in contents: + if entry not in specList and not entry.endswith("-symbolic"): + if args.verbose: print(f"Adding {entry} to Out of Spec") + outOfSpecEntries.append(entry) + +# Pad all lists to be the same length, then zip all three lists together +length = max(len(foundEntries), len(missingEntries), len(outOfSpecEntries)) + +foundEntries = padList(foundEntries, length) +missingEntries = padList(missingEntries, length) +outOfSpecEntries = padList(outOfSpecEntries, length) + +zippedLists = [(a, b, c) for a, b, c in zip(foundEntries, missingEntries, + outOfSpecEntries)] + +with open(args.reportf, 'w', newline='') as file: + fWriter = csv.writer(file) + fWriter.writerow(["Found", "Missing", "Out of Spec"]) + fWriter.writerows(zippedLists) + print(f"Report written to {os.getcwd()}/{args.reportf}") + diff --git a/scripts/fd.o-icon-spec-v0.8.90.txt b/scripts/fd.o-icon-spec-v0.8.90.txt new file mode 100644 index 000000000..5b0427e20 --- /dev/null +++ b/scripts/fd.o-icon-spec-v0.8.90.txt @@ -0,0 +1,540 @@ +// Freedesktop.org Icon Naming Specification +// Version 0.8.90 +// Date: 2025-02-05 +// https://specifications.freedesktop.org/icon-naming-spec/latest/ + +address-book-new +application-exit +appointment-new +call-start +call-stop +contact-new +document-new +document-open +document-open-recent +document-page-setup +document-print +document-print-preview +document-properties +document-revert +document-save +document-save-as +document-send +edit-clear +edit-copy +edit-cut +edit-delete +edit-find +edit-find-replace +edit-paste +edit-redo +edit-select-all +edit-undo +folder-new +format-indent-less +format-indent-more +format-justify-center +format-justify-fill +format-justify-left +format-justify-right +format-text-direction-ltr +format-text-direction-rtl +format-text-bold +format-text-italic +format-text-underline +format-text-strikethrough +go-bottom +go-down +go-first +go-home +go-jump +go-last +go-next +go-previous +go-top +go-up +help-about +help-contents +help-faq +insert-image +insert-link +insert-object +insert-text +list-add +list-remove +mail-forward +mail-mark-important +mail-mark-junk +mail-mark-notjunk +mail-mark-read +mail-mark-unread +mail-message-new +mail-reply-all +mail-reply-sender +mail-send +mail-send-receive +media-eject +media-playback-pause +media-playback-start +media-playback-stop +media-record +media-seek-backward +media-seek-forward +media-skip-backward +media-skip-forward +object-flip-horizontal +object-flip-vertical +object-rotate-left +object-rotate-right +process-stop +system-lock-screen +system-log-out +system-run +system-search +system-reboot +system-shutdown +tools-check-spelling +view-fullscreen +view-refresh +view-restore +view-sort-ascending +view-sort-descending +window-close +window-new +zoom-fit-best +zoom-in +zoom-original +zoom-out +process-working +accessories-calculator +accessories-character-map +accessories-dictionary +accessories-screenshot-tool +accessories-text-editor +help-browser +multimedia-volume-control +preferences-desktop-accessibility +preferences-desktop-font +preferences-desktop-keyboard +preferences-desktop-locale +preferences-desktop-multimedia +preferences-desktop-screensaver +preferences-desktop-theme +preferences-desktop-wallpaper +system-file-manager +system-software-install +system-software-update +utilities-system-monitor +utilities-terminal +applications-accessories +applications-development +applications-engineering +applications-games +applications-graphics +applications-internet +applications-multimedia +applications-office +applications-other +applications-science +applications-system +applications-utilities +preferences-desktop +preferences-desktop-peripherals +preferences-desktop-personal +preferences-other +preferences-system +preferences-system-network +system-help +audio-card +audio-input-microphone +battery +camera-photo +camera-video +camera-web +computer +drive-harddisk +drive-optical +drive-removable-media +input-gaming +input-keyboard +input-mouse +input-tablet +media-flash +media-floppy +media-optical +media-tape +modem +multimedia-player +network-wired +network-wireless +pda +phone +printer +scanner +video-display +emblem-default +emblem-documents +emblem-downloads +emblem-favorite +emblem-important +emblem-mail +emblem-photos +emblem-readonly +emblem-shared +emblem-symbolic-link +emblem-synchronized +emblem-system +emblem-unreadable +face-angel +face-angry +face-cool +face-crying +face-devilish +face-embarrassed +face-kiss +face-laugh +face-monkey +face-plain +face-raspberry +face-sad +face-sick +face-smile +face-smile-big +face-smirk +face-surprise +face-tired +face-uncertain +face-wink +face-worried +flag-AF +flag-AX +flag-AL +flag-DZ +flag-AS +flag-AD +flag-AO +flag-AI +flag-AQ +flag-AG +flag-AR +flag-AM +flag-AW +flag-AU +flag-AT +flag-AZ +flag-BS +flag-BH +flag-BD +flag-BB +flag-BY +flag-BE +flag-BZ +flag-BJ +flag-BM +flag-BT +flag-BO +flag-BQ +flag-BA +flag-BW +flag-BV +flag-BR +flag-IO +flag-BN +flag-BG +flag-BF +flag-BI +flag-CV +flag-KH +flag-CM +flag-CA +flag-KY +flag-CF +flag-TD +flag-CL +flag-CN +flag-CX +flag-CC +flag-CO +flag-KM +flag-CG +flag-CD +flag-CK +flag-CR +flag-CI +flag-HR +flag-CU +flag-CW +flag-CY +flag-CZ +flag-DK +flag-DJ +flag-DM +flag-DO +flag-EC +flag-EG +flag-SV +flag-GQ +flag-ER +flag-EE +flag-SZ +flag-ET +flag-FK +flag-FO +flag-FJ +flag-FI +flag-FR +flag-GF +flag-PF +flag-TF +flag-GA +flag-GM +flag-GE +flag-DE +flag-GH +flag-GI +flag-GR +flag-GL +flag-GD +flag-GP +flag-GU +flag-GT +flag-GG +flag-GN +flag-GW +flag-GY +flag-HT +flag-HM +flag-VA +flag-HN +flag-HK +flag-HU +flag-IS +flag-IN +flag-ID +flag-IR +flag-IQ +flag-IE +flag-IM +flag-IL +flag-IT +flag-JM +flag-JP +flag-JE +flag-JO +flag-KZ +flag-KE +flag-KI +flag-KP +flag-KR +flag-KW +flag-KG +flag-LA +flag-LV +flag-LB +flag-LS +flag-LR +flag-LY +flag-LI +flag-LT +flag-LU +flag-MO +flag-MG +flag-MW +flag-MY +flag-MV +flag-ML +flag-MT +flag-MH +flag-MQ +flag-MR +flag-MU +flag-YT +flag-MX +flag-FM +flag-MD +flag-MC +flag-MN +flag-ME +flag-MS +flag-MA +flag-MZ +flag-MM +flag-NA +flag-NR +flag-NP +flag-NL +flag-NC +flag-NZ +flag-NI +flag-NE +flag-NG +flag-NU +flag-NF +flag-MK +flag-MP +flag-NO +flag-OM +flag-PK +flag-PW +flag-PS +flag-PA +flag-PG +flag-PY +flag-PE +flag-PH +flag-PN +flag-PL +flag-PT +flag-PR +flag-QA +flag-RE +flag-RO +flag-RU +flag-RW +flag-BL +flag-SH +flag-KN +flag-LC +flag-MF +flag-PM +flag-VC +flag-WS +flag-SM +flag-ST +flag-SA +flag-SN +flag-RS +flag-SC +flag-SL +flag-SG +flag-SX +flag-SK +flag-SI +flag-SB +flag-SO +flag-ZA +flag-GS +flag-SS +flag-ES +flag-LK +flag-SD +flag-SR +flag-SJ +flag-SE +flag-CH +flag-SY +flag-TW +flag-TJ +flag-TZ +flag-TH +flag-TL +flag-TG +flag-TK +flag-TO +flag-TT +flag-TN +flag-TR +flag-TM +flag-TC +flag-TV +flag-UG +flag-UA +flag-AE +flag-GB +flag-US +flag-UM +flag-UY +flag-UZ +flag-VU +flag-VE +flag-VN +flag-VG +flag-VI +flag-WF +flag-EH +flag-YE +flag-ZM +flag-ZW +application-x-executable +audio-x-generic +font-x-generic +image-x-generic +package-x-generic +text-html +text-x-generic +text-x-generic-template +text-x-script +video-x-generic +x-office-address-book +x-office-calendar +x-office-document +x-office-presentation +x-office-spreadsheet +folder +folder-remote +network-server +network-workgroup +start-here +user-bookmarks +user-desktop +user-home +user-trash +appointment-missed +appointment-soon +audio-volume-high +audio-volume-low +audio-volume-medium +audio-volume-muted +battery-caution +battery-low +dialog-error +dialog-information +dialog-password +dialog-question +dialog-warning +folder-drag-accept +folder-open +folder-visiting +image-loading +image-missing +mail-attachment +mail-unread +mail-read +mail-replied +mail-signed +mail-signed-verified +media-playlist-repeat +media-playlist-shuffle +network-error +network-idle +network-offline +network-receive +network-transmit +network-transmit-receive +printer-error +printer-printing +security-high +security-medium +security-low +software-update-available +software-update-urgent +sync-error +sync-synchronizing +task-due +task-past-due +user-available +user-away +user-idle +user-offline +user-trash-full +weather-clear +weather-clear-night +weather-few-clouds +weather-few-clouds-night +weather-fog +weather-overcast +weather-severe-alert +weather-showers +weather-showers-scattered +weather-snow +weather-storm