From edb6a2d751f0b1193374537eb50ecbf644cafe82 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Thu, 6 Feb 2025 17:41:05 -0500 Subject: [PATCH 01/23] add script and spec file --- scripts/audit.py | 156 +++++++++ scripts/fd.o-icon-spec-v0.8.90.txt | 537 +++++++++++++++++++++++++++++ 2 files changed, 693 insertions(+) create mode 100644 scripts/audit.py create mode 100644 scripts/fd.o-icon-spec-v0.8.90.txt diff --git a/scripts/audit.py b/scripts/audit.py new file mode 100644 index 000000000..6c7f26d68 --- /dev/null +++ b/scripts/audit.py @@ -0,0 +1,156 @@ +import os +from sys import argv + +ignoreList = [] + +try: + viewWidth = os.get_terminal_size().columns +except: + viewWidth = 80 + +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 __debug__: + 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 __debug__: + print(f"Adding file {entry.path}, named {entry.name} to SubTree") + tree.append(entry.name.removesuffix(".svg")) + + if __debug__: + 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. +fName = argv[1] +specification = {} + +with open(fName, "r") as file: + print(f"Loading specification in file: {fName}...") + for line in file: + if line.startswith(("//", " ", "\n")): + continue + specification.setdefault(line.removesuffix("\n"), False) + +symbolicSpecification = specification.copy() +print("Successfully loaded specfication file!") + +# 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 = argv.pop(2) +except: + print("Please specify a directory") + exit() +else: + 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 __debug__: + 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 __debug__: + print(f"Found dir at {entry.path}") + subDirTree = populateFromDir(entry) + contents.extend(subDirTree) + else: + if __debug__: + print(f"Adding file {entry.path} to tree") + contents.append(entry.name.removesuffix(".svg")) + +# remove duplicate names from the list +contents = list(set(contents)) + +if __debug__: + 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 = 0 + +for value in specification.values(): + # Increase the number of existant entries if value=True + existantColorEntries += 1 if value else 0 + +print(f"{existantColorEntries / totalEntries * 100:.2f}% coverage of FD.o specification, color entries") + +print('-' * viewWidth) + +existantSymbolicEntries = 0 + +# 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!") + + +for value in symbolicSpecification.values(): + # Increase the number of existant entries if value=True + existantSymbolicEntries += 1 if value else 0 + +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 = 0 + +for value in results: + # Increase the number of existant entries if value=True + existantEntries += 1 if value else 0 + +print(f"{existantEntries / totalEntries * 100:.2f}% coverage of FD.o specification, all entries") + 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..abf60b4b2 --- /dev/null +++ b/scripts/fd.o-icon-spec-v0.8.90.txt @@ -0,0 +1,537 @@ +// Freedesktop.org Icon Naming Specification +// Version 0.8.90 +// Date: 2025-02-05 + +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 +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 +preferences-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-sychronized +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 From e1961f1c82ac2709d636e8dbdf90c5ad49cce032 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Fri, 7 Feb 2025 13:55:04 -0500 Subject: [PATCH 02/23] Output list of files in csv file --- scripts/audit.py | 100 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 70 insertions(+), 30 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index 6c7f26d68..9e7960065 100644 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -1,4 +1,4 @@ -import os +import os, csv from sys import argv ignoreList = [] @@ -8,6 +8,19 @@ except: viewWidth = 80 +try: + outputFName = argv[4] +except: + outputFName = "report.csv" + +def padList(iterable: list, length: int) -> list: + extensionLength = max(length - len(iterable), 0) + if __debug__: 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 = [] @@ -15,17 +28,14 @@ def populateFromDir(dir: os.DirEntry) -> set: for entry in os.scandir(dir.path): if entry.name not in ignoreList: if entry.is_dir(): - if __debug__: - print(f"Found dir at {entry.path}, going deeper") + if __debug__: 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 __debug__: - print(f"Adding file {entry.path}, named {entry.name} to SubTree") + if __debug__: print(f"Adding file {entry.path}, named {entry.name} to SubTree") tree.append(entry.name.removesuffix(".svg")) - if __debug__: - print(f"The SubTree for Dir {dir.name} is {tree}") + if __debug__: print(f"The SubTree for Dir {dir.name} is {tree}") return list(set(tree)) @@ -55,6 +65,8 @@ def populateFromDir(dir: os.DirEntry) -> set: 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) @@ -67,8 +79,7 @@ def populateFromDir(dir: os.DirEntry) -> set: print("No .auditignore file found in root") else: print("Loaded .auditignore file!") - if __debug__: - print(f"Values contained: {ignoreList}") + if __debug__: print(f"Values contained: {ignoreList}") print('-' * viewWidth) @@ -79,14 +90,13 @@ def populateFromDir(dir: os.DirEntry) -> set: for entry in os.scandir(): if entry.name not in ignoreList: if entry.is_dir(): - if __debug__: - print(f"Found dir at {entry.path}") + if __debug__: print(f"Found dir at {entry.path}") subDirTree = populateFromDir(entry) contents.extend(subDirTree) else: - if __debug__: - print(f"Adding file {entry.path} to tree") - contents.append(entry.name.removesuffix(".svg")) + if __debug__: 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)) @@ -109,18 +119,12 @@ def populateFromDir(dir: os.DirEntry) -> set: # Calculate percent spec coverage totalEntries = len(specification.keys()) -existantColorEntries = 0 - -for value in specification.values(): - # Increase the number of existant entries if value=True - existantColorEntries += 1 if value else 0 +existantColorEntries = list(specification.values()).count(True) print(f"{existantColorEntries / totalEntries * 100:.2f}% coverage of FD.o specification, color entries") print('-' * viewWidth) -existantSymbolicEntries = 0 - # Check whether things are included in symbolic entries for entry in symbolicSpecification.keys(): extendedEntry = entry + "-symbolic" @@ -130,10 +134,7 @@ def populateFromDir(dir: os.DirEntry) -> set: else: print(f"[!!] {extendedEntry} is missing!") - -for value in symbolicSpecification.values(): - # Increase the number of existant entries if value=True - existantSymbolicEntries += 1 if value else 0 +existantSymbolicEntries = list(symbolicSpecification.values()).count(True) print(f"{existantSymbolicEntries / totalEntries * 100:.2f}% coverage of FD.o specification, symbolic entries") @@ -146,11 +147,50 @@ def populateFromDir(dir: os.DirEntry) -> set: for i, value in enumerate(colorResults): results.append(symbolicResults[i] | value) -existantEntries = 0 - -for value in results: - # Increase the number of existant entries if value=True - existantEntries += 1 if value else 0 +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 = [] + +for i, result in enumerate(results): + # If the result is true, then it was found + # If the result is false, then it is missing + name = specList[i] + if result: + if __debug__: print(f"Adding {name} to Found...") + foundEntries.append(name) + else: + if __debug__: 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: + if __debug__: 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(outputFName, '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()}/{outputFName}") From 582d542eac39cd161c79dc30250d283d0d1f69db Mon Sep 17 00:00:00 2001 From: William Kelso Date: Fri, 7 Feb 2025 14:00:46 -0500 Subject: [PATCH 03/23] Add help usage message --- scripts/audit.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/audit.py b/scripts/audit.py index 9e7960065..92193e914 100644 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -3,6 +3,10 @@ ignoreList = [] +if argv[1] == "-h": + print("USAGE: python (-O) audit.py ()") + exit() + try: viewWidth = os.get_terminal_size().columns except: From 679b88e02f85e7d2a228ceba132c25b8454184e4 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Wed, 12 Feb 2025 14:23:11 -0500 Subject: [PATCH 04/23] correct noted entries --- scripts/fd.o-icon-spec-v0.8.90.txt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/scripts/fd.o-icon-spec-v0.8.90.txt b/scripts/fd.o-icon-spec-v0.8.90.txt index abf60b4b2..5b0427e20 100644 --- a/scripts/fd.o-icon-spec-v0.8.90.txt +++ b/scripts/fd.o-icon-spec-v0.8.90.txt @@ -1,6 +1,7 @@ // 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 @@ -83,6 +84,8 @@ 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 @@ -141,7 +144,7 @@ preferences-desktop-personal preferences-other preferences-system preferences-system-network -preferences-help +system-help audio-card audio-input-microphone battery @@ -179,7 +182,7 @@ emblem-photos emblem-readonly emblem-shared emblem-symbolic-link -emblem-sychronized +emblem-synchronized emblem-system emblem-unreadable face-angel From aef1a2916fd932d5d21970bcbdb89b145cf78b37 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Wed, 12 Feb 2025 14:29:07 -0500 Subject: [PATCH 05/23] Exclude symbolic icons from report --- scripts/audit.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index 92193e914..783805f06 100644 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -42,7 +42,6 @@ def populateFromDir(dir: os.DirEntry) -> set: if __debug__: 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. @@ -90,7 +89,6 @@ def populateFromDir(dir: os.DirEntry) -> set: # Generate the contents of the relevant directory tree contents = [] - for entry in os.scandir(): if entry.name not in ignoreList: if entry.is_dir(): @@ -165,7 +163,8 @@ def populateFromDir(dir: os.DirEntry) -> set: missingEntries = [] outOfSpecEntries = [] -for i, result in enumerate(results): +# 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] @@ -198,3 +197,4 @@ def populateFromDir(dir: os.DirEntry) -> set: fWriter.writerow(["Found", "Missing", "Out of Spec"]) fWriter.writerows(zippedLists) print(f"Report written to {os.getcwd()}/{outputFName}") + From d016dadb9038f59dd08bd10578fbb1a8270ec3d6 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Wed, 12 Feb 2025 14:39:05 -0500 Subject: [PATCH 06/23] Symbolic is common practice enough that showing it in out-of-spec isn't helpful --- scripts/audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/audit.py b/scripts/audit.py index 783805f06..2bc22dc77 100644 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -178,7 +178,7 @@ def populateFromDir(dir: os.DirEntry) -> set: # 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: + if entry not in specList and not entry.endswith("-symbolic"): if __debug__: print(f"Adding {entry} to Out of Spec") outOfSpecEntries.append(entry) From b7a7f57dfd4f79f1c546072351ed3416197503db Mon Sep 17 00:00:00 2001 From: William Kelso Date: Mon, 24 Feb 2025 13:18:37 -0500 Subject: [PATCH 07/23] add optional support for pulling the spec off the web --- scripts/audit.py | 58 ++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 51 insertions(+), 7 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index 2bc22dc77..12d553fae 100644 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import os, csv from sys import argv @@ -7,6 +9,7 @@ print("USAGE: python (-O) audit.py ()") exit() +# make hrules pretty try: viewWidth = os.get_terminal_size().columns except: @@ -17,6 +20,41 @@ except: outputFName = "report.csv" +# 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: string) -> 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(item) + + return result + def padList(iterable: list, length: int) -> list: extensionLength = max(length - len(iterable), 0) if __debug__: print(f"Extending list by {extensionLength} elements") @@ -48,15 +86,21 @@ def populateFromDir(dir: os.DirEntry) -> set: fName = argv[1] specification = {} -with open(fName, "r") as file: - print(f"Loading specification in file: {fName}...") - for line in file: - if line.startswith(("//", " ", "\n")): - continue - specification.setdefault(line.removesuffix("\n"), False) +if foundDeps: + url = "" + soup = get_soup(url) + specList = parse_soup(soup) + specification.fromkeys(specList, False) +else: + with open(fName, "r") as file: + print(f"Loading specification in file: {fName}...") + for line in file: + if line.startswith(("//", " ", "\n")): + continue + specification.setdefault(line.removesuffix("\n"), False) +print("Successfully loaded specfication!") symbolicSpecification = specification.copy() -print("Successfully loaded specfication file!") # 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 From ac844364c6ba91498dd63fb31ec23837a7066d1f Mon Sep 17 00:00:00 2001 From: William Kelso Date: Mon, 24 Feb 2025 14:17:14 -0500 Subject: [PATCH 08/23] Clean up remote pull and use argparse like a civilized script --- scripts/audit.py | 94 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 63 insertions(+), 31 deletions(-) mode change 100644 => 100755 scripts/audit.py diff --git a/scripts/audit.py b/scripts/audit.py old mode 100644 new mode 100755 index 12d553fae..a2827f36b --- a/scripts/audit.py +++ b/scripts/audit.py @@ -1,13 +1,33 @@ #!/usr/bin/env python3 -import os, csv -from sys import argv +import os, csv, argparse ignoreList = [] -if argv[1] == "-h": - print("USAGE: python (-O) audit.py ()") - exit() +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: @@ -15,11 +35,6 @@ except: viewWidth = 80 -try: - outputFName = argv[4] -except: - outputFName = "report.csv" - # Check for optional dependencies to enable drawing the spec from the web. # Otherwise fall back to included file try: @@ -32,7 +47,7 @@ print("Found bs4 and requests dependencies, pulling spec from the web") foundDeps = True -def get_soup(url: string) -> BeautifulSoup: +def get_soup(url: str) -> BeautifulSoup: page = requests.get(url) soup = BeautifulSoup(page.text, "html.parser") return soup @@ -51,13 +66,29 @@ def parse_soup(soup: BeautifulSoup) -> list: name = text[0].text.strip().replace("\n", "") # skip the header row if name == "Name": continue - result.append(item) + 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 __debug__: print(f"Extending list by {extensionLength} elements") + if args.verbose: print(f"Extending list by {extensionLength} elements") for _ in range(extensionLength): iterable.append(None) @@ -70,30 +101,31 @@ def populateFromDir(dir: os.DirEntry) -> set: for entry in os.scandir(dir.path): if entry.name not in ignoreList: if entry.is_dir(): - if __debug__: print(f"Found dir at {entry.path}, going deeper") + 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 __debug__: print(f"Adding file {entry.path}, named {entry.name} to SubTree") + if args.verbose: print(f"Adding file {entry.path}, named {entry.name} to SubTree") tree.append(entry.name.removesuffix(".svg")) - if __debug__: print(f"The SubTree for Dir {dir.name} is {tree}") + 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. -fName = argv[1] specification = {} if foundDeps: - url = "" + url = "https://specifications.freedesktop.org/icon-naming-spec/latest/" soup = get_soup(url) specList = parse_soup(soup) - specification.fromkeys(specList, False) + specList.extend(get_iso_3166()) + specList.remove("flag-aa") + specification = specification.fromkeys(specList, False) else: - with open(fName, "r") as file: - print(f"Loading specification in file: {fName}...") + with open(args.specf, "r") as file: + print(f"Loading specification in file: {specf}...") for line in file: if line.startswith(("//", " ", "\n")): continue @@ -107,7 +139,7 @@ def populateFromDir(dir: os.DirEntry) -> set: # a list. Anything in this list doesn't exist as far as this script is # concerned. try: - targetDir = argv.pop(2) + targetDir = args.themePath except: print("Please specify a directory") exit() @@ -126,7 +158,7 @@ def populateFromDir(dir: os.DirEntry) -> set: print("No .auditignore file found in root") else: print("Loaded .auditignore file!") - if __debug__: print(f"Values contained: {ignoreList}") + if args.verbose: print(f"Values contained: {ignoreList}") print('-' * viewWidth) @@ -136,18 +168,18 @@ def populateFromDir(dir: os.DirEntry) -> set: for entry in os.scandir(): if entry.name not in ignoreList: if entry.is_dir(): - if __debug__: print(f"Found dir at {entry.path}") + if args.verbose: print(f"Found dir at {entry.path}") subDirTree = populateFromDir(entry) contents.extend(subDirTree) else: - if __debug__: print(f"Adding file {entry.path} to tree") + 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 __debug__: +if args.verbose: print(f"The full dirTree is {contents}") print('-' * viewWidth) @@ -213,17 +245,17 @@ def populateFromDir(dir: os.DirEntry) -> set: # If the result is false, then it is missing name = specList[i] if result: - if __debug__: print(f"Adding {name} to Found...") + if args.verbose: print(f"Adding {name} to Found...") foundEntries.append(name) else: - if __debug__: print(f"Adding {name} to Missing...") + 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 __debug__: print(f"Adding {entry} to Out of Spec") + 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 @@ -236,9 +268,9 @@ def populateFromDir(dir: os.DirEntry) -> set: zippedLists = [(a, b, c) for a, b, c in zip(foundEntries, missingEntries, outOfSpecEntries)] -with open(outputFName, 'w', newline='') as file: +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()}/{outputFName}") + print(f"Report written to {os.getcwd()}/{args.reportf}") From da94c8ed48eea7cbf5da832bb30583187e6e0cfe Mon Sep 17 00:00:00 2001 From: William Kelso Date: Tue, 17 Feb 2026 13:18:56 -0500 Subject: [PATCH 09/23] document script and add .venv to gitignore --- .gitignore | 1 + CONTRIBUTING.md | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/.gitignore b/.gitignore index 5975d974f..9feb88a69 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ *~ build/ +.venv/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 174140b85..cebe38073 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,5 +28,17 @@ Since this set is designed specifically for elementary OS, pull requests to add Use of icon names in line with the [FreeDesktop.Org Icon Naming Specification](http://standards.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html) is encouraged. +To monitor coverage, an auditing python script is included under `/scripts`. While this +script can be used without any extra dependencies, setting up a virtual +environment and installing `requests` and `BeautifulSoup4` enables functionality +that ensures all resources pulled are as current as possible. To do this: + +1. From the repository root, run `python3 -m venv .venv`. This sets up a python + virtual environment in a hidden folder, scoped to this repository. +2. To activate the venv, run `source .venv/bin/activate`. +3. To install `requests` and `BeautifulSoup4`, run `python3 -m pip install requests + BeautifulSoup4` +4. Run the script with `python3 scripts/audit.py ` + ## Third-Party Brand Preservation elementary icons do not attempt to supply icons for third-party apps. Pull requests to add icons or symbolic links that would overwrite the branding of third-party apps will be rejected. From 5ab651139e84b623d3cea987a5e324fdc3e68c31 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Tue, 17 Feb 2026 13:42:19 -0500 Subject: [PATCH 10/23] mention python3 system package reqs --- CONTRIBUTING.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cebe38073..c8a502a03 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,12 +33,13 @@ script can be used without any extra dependencies, setting up a virtual environment and installing `requests` and `BeautifulSoup4` enables functionality that ensures all resources pulled are as current as possible. To do this: -1. From the repository root, run `python3 -m venv .venv`. This sets up a python +1. If not already installed, run `apt install python3 python3.12-venv` +2. From the repository root, run `python3 -m venv .venv`. This sets up a python virtual environment in a hidden folder, scoped to this repository. -2. To activate the venv, run `source .venv/bin/activate`. -3. To install `requests` and `BeautifulSoup4`, run `python3 -m pip install requests +3. To activate the venv, run `source .venv/bin/activate`. +4. To install `requests` and `BeautifulSoup4`, run `python3 -m pip install requests BeautifulSoup4` -4. Run the script with `python3 scripts/audit.py ` +5. Run the script with `python3 scripts/audit.py ` ## Third-Party Brand Preservation elementary icons do not attempt to supply icons for third-party apps. Pull requests to add icons or symbolic links that would overwrite the branding of third-party apps will be rejected. From 317f3e2d20a5b9c48706873437914ec824874c27 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Tue, 17 Feb 2026 14:06:09 -0500 Subject: [PATCH 11/23] set a user agent for requests --- scripts/audit.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index a2827f36b..515259aff 100755 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -48,7 +48,7 @@ foundDeps = True def get_soup(url: str) -> BeautifulSoup: - page = requests.get(url) + page = session.get(url) soup = BeautifulSoup(page.text, "html.parser") return soup @@ -72,9 +72,10 @@ def parse_soup(soup: BeautifulSoup) -> list: def get_iso_3166() -> list: result = [] - page = requests.get("https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2") + page = session.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 + print(soup) + table = soup.find_all("table", class_="wikitable")[3].tbody rows = table.find_all("tr") for row in rows: code = row.text.strip().replace("\n", " ").split()[0] @@ -118,6 +119,13 @@ def populateFromDir(dir: os.DirEntry) -> set: if foundDeps: url = "https://specifications.freedesktop.org/icon-naming-spec/latest/" + headers = { + 'User-Agent': 'fdo-icon-name-spec-audit-script/1.0.0', + } + + session = requests.Session() + session.headers.update(headers) + soup = get_soup(url) specList = parse_soup(soup) specList.extend(get_iso_3166()) From 13df2f54839b64e3671d1707066d76adc3d40631 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Tue, 17 Feb 2026 14:13:43 -0500 Subject: [PATCH 12/23] rm debug print --- scripts/audit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/audit.py b/scripts/audit.py index 515259aff..71ec4f9d2 100755 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -74,7 +74,6 @@ def get_iso_3166() -> list: result = [] page = session.get("https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2") soup = BeautifulSoup(page.text, "html.parser") - print(soup) table = soup.find_all("table", class_="wikitable")[3].tbody rows = table.find_all("tr") for row in rows: From e17736baa035f00134e6b99b3c427fdbfe3dde9f Mon Sep 17 00:00:00 2001 From: William Kelso Date: Thu, 19 Feb 2026 21:35:54 -0500 Subject: [PATCH 13/23] pythonify and restrict considered files to svg --- scripts/audit.py | 182 ++++++++++++++++++++++++++--------------------- 1 file changed, 100 insertions(+), 82 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index 71ec4f9d2..3879400b2 100755 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -1,15 +1,27 @@ #!/usr/bin/env python3 -import os, csv, argparse +# # Fd.o Icon Name Specification Audit + +# ## Description: +# +# Audit a Freedesktop.org icon specification-compatible theme against the icon +# naming specification, returning a csv report of what icons were included that are +# in-spec, what icons were included that are out-of-spec, and what icons were +# missing that are included in the spec. + +# ## Dependencies: +# +# - requests (optional) +# - bs4 (optional) -ignoreList = [] +import os, csv, argparse parser = argparse.ArgumentParser( prog = "audit", description = "Audit a FreeDesktop.org compatible icon set against a specification" ) parser.add_argument( - "themePath", + "theme_path", ) parser.add_argument( "-s", @@ -31,9 +43,9 @@ # make hrules pretty try: - viewWidth = os.get_terminal_size().columns + view_width = os.get_terminal_size().columns except: - viewWidth = 80 + view_width = 80 # Check for optional dependencies to enable drawing the spec from the web. # Otherwise fall back to included file @@ -41,11 +53,11 @@ import requests from bs4 import BeautifulSoup except: - print("Couldn't find bs4 and requests dependencies, falling back to using spec file...") - foundDeps = False + print("Couldn't find bs4 and requests dependencies, falling back to using spec file…") + found_deps = False else: print("Found bs4 and requests dependencies, pulling spec from the web") - foundDeps = True + found_deps = True def get_soup(url: str) -> BeautifulSoup: page = session.get(url) @@ -86,27 +98,33 @@ def get_iso_3166() -> list: -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): +def pad_list(iterable: list, length: int) -> list: + extension_length = max(length - len(iterable), 0) + if args.verbose: print(f"Extending list by {extension_length} elements") + for _ in range(extension_length): iterable.append(None) return iterable -def populateFromDir(dir: os.DirEntry) -> set: +# Place this in a top level context so we don't have to pass it around +ignore_list = [] + +def populate_from_dir(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 entry.name in ignore_list: + continue + + 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(populate_from_dir(entry)) + elif entry.name.endswith(".svg"): + # We only check svg because that's the only type we use in elementary icons + 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)) @@ -116,7 +134,7 @@ def populateFromDir(dir: os.DirEntry) -> set: # Iterate over the list, checking to see if the specified file can be found. specification = {} -if foundDeps: +if found_deps: url = "https://specifications.freedesktop.org/icon-naming-spec/latest/" headers = { 'User-Agent': 'fdo-icon-name-spec-audit-script/1.0.0', @@ -126,61 +144,61 @@ def populateFromDir(dir: os.DirEntry) -> set: session.headers.update(headers) soup = get_soup(url) - specList = parse_soup(soup) - specList.extend(get_iso_3166()) - specList.remove("flag-aa") - specification = specification.fromkeys(specList, False) + spec_list = parse_soup(soup) + spec_list.extend(get_iso_3166()) + spec_list.remove("flag-aa") + specification = specification.fromkeys(spec_list, False) else: with open(args.specf, "r") as file: - print(f"Loading specification in file: {specf}...") + print(f"Loading specification in file: {specf}…") for line in file: if line.startswith(("//", " ", "\n")): continue - specification.setdefault(line.removesuffix("\n"), False) + specification.setdefault(line.remove_suffix("\n"), False) print("Successfully loaded specfication!") -symbolicSpecification = specification.copy() +symbolic_specification = 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 + target_dir = args.theme_path except: print("Please specify a directory") exit() else: # Save the cwd so we can switch back later - scriptDir = os.getcwd() - dir = os.fspath(targetDir) + script_dir = os.getcwd() + dir = os.fspath(target_dir) os.chdir(dir) try: with open(".auditignore", "r") as file: print("Found .auditignore file!") for line in file: - ignoreList.append(line.removesuffix("\n")) + ignore_list.append(line.strip()) except: print("No .auditignore file found in root") else: print("Loaded .auditignore file!") - if args.verbose: print(f"Values contained: {ignoreList}") + if args.verbose: print(f"Values contained: {ignore_list}") -print('-' * viewWidth) +print('-' * view_width) # Generate the contents of the relevant directory tree contents = [] for entry in os.scandir(): - if entry.name not in ignoreList: + if entry.name not in ignore_list: if entry.is_dir(): if args.verbose: print(f"Found dir at {entry.path}") - subDirTree = populateFromDir(entry) - contents.extend(subDirTree) + sub_dir_tree = populate_from_dir(entry) + contents.extend(sub_dir_tree) else: - if args.verbose: print(f"Adding file {entry.path} to tree") if entry.name.endswith(".svg"): + if args.verbose: print(f"Adding file {entry.path} to tree") contents.append(entry.name.removesuffix(".svg")) # remove duplicate names from the list @@ -188,7 +206,7 @@ def populateFromDir(dir: os.DirEntry) -> set: if args.verbose: print(f"The full dirTree is {contents}") - print('-' * viewWidth) + print('-' * view_width) # 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 @@ -203,81 +221,81 @@ def populateFromDir(dir: os.DirEntry) -> set: # Calculate percent spec coverage -totalEntries = len(specification.keys()) -existantColorEntries = list(specification.values()).count(True) +total_entries = len(specification.keys()) +existant_color_entries = list(specification.values()).count(True) -print(f"{existantColorEntries / totalEntries * 100:.2f}% coverage of FD.o specification, color entries") +print(f"{existant_color_entries / total_entries * 100:.2f}% coverage of FD.o specification, color entries") -print('-' * viewWidth) +print('-' * view_width) # 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} +for entry in symbolic_specification.keys(): + extended_entry = entry + "-symbolic" + if extended_entry in contents: + print(f"Found {extended_entry}") + symbolic_specification |= {entry: True} else: - print(f"[!!] {extendedEntry} is missing!") + print(f"[!!] {extended_entry} is missing!") -existantSymbolicEntries = list(symbolicSpecification.values()).count(True) +existant_symbolic_entries = list(symbolic_specification.values()).count(True) -print(f"{existantSymbolicEntries / totalEntries * 100:.2f}% coverage of FD.o specification, symbolic entries") +print(f"{existant_symbolic_entries / total_entries * 100:.2f}% coverage of FD.o specification, symbolic entries") -print('-' * viewWidth) +print('-' * view_width) # Merging results and comparing all entries -colorResults = list(specification.values()) -symbolicResults = list(symbolicSpecification.values()) +color_results = list(specification.values()) +symbolic_results = list(symbolic_specification.values()) results = [] -for i, value in enumerate(colorResults): - results.append(symbolicResults[i] | value) +for i, value in enumerate(color_results): + results.append(symbolic_results[i] | value) -existantEntries = results.count(True) +existant_entries = results.count(True) -print(f"{existantEntries / totalEntries * 100:.2f}% coverage of FD.o specification, all entries") +print(f"{existant_entries / total_entries * 100:.2f}% coverage of FD.o specification, all entries") -print('-' * viewWidth) -os.chdir(scriptDir) +print('-' * view_width) +os.chdir(script_dir) # Write report file with Found, Missing, and Out-of-Spec information -specList = list(specification.keys()) -foundEntries = [] -missingEntries = [] -outOfSpecEntries = [] +spec_list = list(specification.keys()) +found_entries = [] +missing_entries = [] +out_of_spec_entries = [] # We only look at the color entry results, as symbolic is not officially in the spec -for i, result in enumerate(colorResults): +for i, result in enumerate(color_results): # If the result is true, then it was found # If the result is false, then it is missing - name = specList[i] + name = spec_list[i] if result: - if args.verbose: print(f"Adding {name} to Found...") - foundEntries.append(name) + if args.verbose: print(f"Adding {name} to Found…") + found_entries.append(name) else: - if args.verbose: print(f"Adding {name} to Missing...") - missingEntries.append(name) + if args.verbose: print(f"Adding {name} to Missing…") + missing_entries.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 entry not in spec_list and not entry.endswith("-symbolic"): if args.verbose: print(f"Adding {entry} to Out of Spec") - outOfSpecEntries.append(entry) + out_of_spec_entries.append(entry) # Pad all lists to be the same length, then zip all three lists together -length = max(len(foundEntries), len(missingEntries), len(outOfSpecEntries)) +length = max(len(found_entries), len(missing_entries), len(out_of_spec_entries)) -foundEntries = padList(foundEntries, length) -missingEntries = padList(missingEntries, length) -outOfSpecEntries = padList(outOfSpecEntries, length) +found_entries = pad_list(found_entries, length) +missing_entries = pad_list(missing_entries, length) +out_of_spec_entries = pad_list(out_of_spec_entries, length) -zippedLists = [(a, b, c) for a, b, c in zip(foundEntries, missingEntries, - outOfSpecEntries)] +zipped_lists = [(a, b, c) for a, b, c in zip(found_entries, missing_entries, + out_of_spec_entries)] with open(args.reportf, 'w', newline='') as file: - fWriter = csv.writer(file) - fWriter.writerow(["Found", "Missing", "Out of Spec"]) - fWriter.writerows(zippedLists) + fwriter = csv.writer(file) + fwriter.writerow(["Found", "Missing", "Out of Spec"]) + fwriter.writerows(zipped_lists) print(f"Report written to {os.getcwd()}/{args.reportf}") From ebab803e35e320a5b10aaab7894d693b647b6d8b Mon Sep 17 00:00:00 2001 From: William Kelso Date: Fri, 20 Feb 2026 13:04:27 -0500 Subject: [PATCH 14/23] specf called incorrectly --- scripts/audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/audit.py b/scripts/audit.py index 3879400b2..f389023bb 100755 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -150,7 +150,7 @@ def populate_from_dir(dir: os.DirEntry) -> set: specification = specification.fromkeys(spec_list, False) else: with open(args.specf, "r") as file: - print(f"Loading specification in file: {specf}…") + print(f"Loading specification in file: {args.specf}…") for line in file: if line.startswith(("//", " ", "\n")): continue From 76b90cff90bf17359b9609770162361c95b3dca1 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Fri, 20 Feb 2026 13:06:20 -0500 Subject: [PATCH 15/23] correct typos --- scripts/audit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index f389023bb..274a68720 100755 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -56,7 +56,7 @@ print("Couldn't find bs4 and requests dependencies, falling back to using spec file…") found_deps = False else: - print("Found bs4 and requests dependencies, pulling spec from the web") + print("Found bs4 and requests dependencies, pulling spec from the web…") found_deps = True def get_soup(url: str) -> BeautifulSoup: @@ -156,7 +156,7 @@ def populate_from_dir(dir: os.DirEntry) -> set: continue specification.setdefault(line.remove_suffix("\n"), False) -print("Successfully loaded specfication!") +print("Successfully loaded specification!") symbolic_specification = specification.copy() # Change the working dir to our target dir, to make it easier to traverse the From eb31477ba6258d76155ce4cdc5c76da788273e4e Mon Sep 17 00:00:00 2001 From: William Kelso Date: Fri, 20 Feb 2026 13:54:42 -0500 Subject: [PATCH 16/23] add option to specify which list to report --- scripts/audit.py | 46 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index 274a68720..6f2c245aa 100755 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -39,6 +39,13 @@ dest="reportf", default="report" ) +parser.add_argument( + "-d", + "--display-results", + dest="to_display", + choices=["all", "found", "missing", "extra"], + default="all" +) args = parser.parse_args() # make hrules pretty @@ -258,11 +265,11 @@ def populate_from_dir(dir: os.DirEntry) -> set: print('-' * view_width) os.chdir(script_dir) -# Write report file with Found, Missing, and Out-of-Spec information +# Write report file with Found, Missing, and Extra information spec_list = list(specification.keys()) found_entries = [] missing_entries = [] -out_of_spec_entries = [] +extra_entries = [] # We only look at the color entry results, as symbolic is not officially in the spec for i, result in enumerate(color_results): @@ -280,22 +287,33 @@ def populate_from_dir(dir: os.DirEntry) -> set: # point out those that aren't in the spec for entry in contents: if entry not in spec_list and not entry.endswith("-symbolic"): - if args.verbose: print(f"Adding {entry} to Out of Spec") - out_of_spec_entries.append(entry) + if args.verbose: print(f"Adding {entry} to Extra") + extra_entries.append(entry) # Pad all lists to be the same length, then zip all three lists together -length = max(len(found_entries), len(missing_entries), len(out_of_spec_entries)) - -found_entries = pad_list(found_entries, length) -missing_entries = pad_list(missing_entries, length) -out_of_spec_entries = pad_list(out_of_spec_entries, length) - -zipped_lists = [(a, b, c) for a, b, c in zip(found_entries, missing_entries, - out_of_spec_entries)] +length = max(len(found_entries), len(missing_entries), len(extra_entries)) with open(args.reportf, 'w', newline='') as file: fwriter = csv.writer(file) - fwriter.writerow(["Found", "Missing", "Out of Spec"]) - fwriter.writerows(zipped_lists) + match args.to_display: + case "found": + fwriter.writerow(["Found"]) + fwriter.writerows(map(lambda x: [x], found_entries)) + case "missing": + fwriter.writerow(["Missing"]) + fwriter.writerows(map(lambda x: [x], missing_entries)) + case "extra": + fwriter.writerow(["Extra"]) + fwriter.writerows(map(lambda x: [x], extra_entries)) + case _: + fwriter.writerow(["Found", "Missing", "Extra"]) + + found_entries = pad_list(found_entries, length) + missing_entries = pad_list(missing_entries, length) + extra_entries = pad_list(extra_entries, length) + zipped_lists = [(a, b, c) for a, b, c in zip(found_entries, + missing_entries, extra_entries)] + fwriter.writerows(zipped_lists) + print(f"Report written to {os.getcwd()}/{args.reportf}") From faa73e2fe996fa5e1ad0249d5e6162712154c520 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Wed, 25 Feb 2026 14:34:57 -0500 Subject: [PATCH 17/23] remove flag icon code anticipating removal from fdo spec, and we don't intend to ship these --- scripts/audit.py | 18 --- scripts/fd.o-icon-spec-v0.8.90.txt | 249 ----------------------------- 2 files changed, 267 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index 6f2c245aa..0c6a742a9 100755 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -89,22 +89,6 @@ def parse_soup(soup: BeautifulSoup) -> list: return result -def get_iso_3166() -> list: - result = [] - page = session.get("https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2") - soup = BeautifulSoup(page.text, "html.parser") - table = soup.find_all("table", class_="wikitable")[3].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 pad_list(iterable: list, length: int) -> list: extension_length = max(length - len(iterable), 0) if args.verbose: print(f"Extending list by {extension_length} elements") @@ -152,8 +136,6 @@ def populate_from_dir(dir: os.DirEntry) -> set: soup = get_soup(url) spec_list = parse_soup(soup) - spec_list.extend(get_iso_3166()) - spec_list.remove("flag-aa") specification = specification.fromkeys(spec_list, False) else: with open(args.specf, "r") as file: diff --git a/scripts/fd.o-icon-spec-v0.8.90.txt b/scripts/fd.o-icon-spec-v0.8.90.txt index 5b0427e20..a5e1bd0b5 100644 --- a/scripts/fd.o-icon-spec-v0.8.90.txt +++ b/scripts/fd.o-icon-spec-v0.8.90.txt @@ -206,255 +206,6 @@ 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 From 74fc4cf626ab2c5603e9923799e124664bcf1c2c Mon Sep 17 00:00:00 2001 From: William Kelso Date: Thu, 26 Feb 2026 12:50:43 -0500 Subject: [PATCH 18/23] Generalize and don't pull from web if file is specified --- scripts/audit.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index 0c6a742a9..8d35bd6c2 100755 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -57,14 +57,15 @@ # Check for optional dependencies to enable drawing the spec from the web. # Otherwise fall back to included file try: + found_deps = False import requests from bs4 import BeautifulSoup except: print("Couldn't find bs4 and requests dependencies, falling back to using spec file…") - found_deps = False else: - print("Found bs4 and requests dependencies, pulling spec from the web…") - found_deps = True + if args.specf is None: + print("Found bs4 and requests dependencies, pulling spec from the web…") + found_deps = True def get_soup(url: str) -> BeautifulSoup: page = session.get(url) @@ -143,7 +144,7 @@ def populate_from_dir(dir: os.DirEntry) -> set: for line in file: if line.startswith(("//", " ", "\n")): continue - specification.setdefault(line.remove_suffix("\n"), False) + specification.setdefault(line.removesuffix("\n"), False) print("Successfully loaded specification!") symbolic_specification = specification.copy() @@ -213,7 +214,7 @@ def populate_from_dir(dir: os.DirEntry) -> set: total_entries = len(specification.keys()) existant_color_entries = list(specification.values()).count(True) -print(f"{existant_color_entries / total_entries * 100:.2f}% coverage of FD.o specification, color entries") +print(f"{existant_color_entries / total_entries * 100:.2f}% coverage of specification, color entries") print('-' * view_width) @@ -228,7 +229,7 @@ def populate_from_dir(dir: os.DirEntry) -> set: existant_symbolic_entries = list(symbolic_specification.values()).count(True) -print(f"{existant_symbolic_entries / total_entries * 100:.2f}% coverage of FD.o specification, symbolic entries") +print(f"{existant_symbolic_entries / total_entries * 100:.2f}% coverage of specification, symbolic entries") print('-' * view_width) @@ -241,7 +242,7 @@ def populate_from_dir(dir: os.DirEntry) -> set: existant_entries = results.count(True) -print(f"{existant_entries / total_entries * 100:.2f}% coverage of FD.o specification, all entries") +print(f"{existant_entries / total_entries * 100:.2f}% coverage of specification, all entries") print('-' * view_width) From 5e78392d938821a1a896f94870c9397dd71f7bb9 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Mon, 2 Mar 2026 13:07:38 -0500 Subject: [PATCH 19/23] use more standard convetion for output file name --- scripts/audit.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index 8d35bd6c2..001c4ff7d 100755 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -5,7 +5,7 @@ # ## Description: # # Audit a Freedesktop.org icon specification-compatible theme against the icon -# naming specification, returning a csv report of what icons were included that are +# naming specification, returning a report of what icons were included that are # in-spec, what icons were included that are out-of-spec, and what icons were # missing that are included in the spec. @@ -34,8 +34,8 @@ action="store_true" ) parser.add_argument( - "-r", - "--report-name", + "-o", + "--output", dest="reportf", default="report" ) From 7251c08bee43255337cd94043c1b98cded80cc65 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Mon, 2 Mar 2026 13:13:54 -0500 Subject: [PATCH 20/23] flag-gate file output --- scripts/audit.py | 50 ++++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index 001c4ff7d..0f3e95eb5 100755 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -36,8 +36,7 @@ parser.add_argument( "-o", "--output", - dest="reportf", - default="report" + dest="fout", ) parser.add_argument( "-d", @@ -276,27 +275,28 @@ def populate_from_dir(dir: os.DirEntry) -> set: # Pad all lists to be the same length, then zip all three lists together length = max(len(found_entries), len(missing_entries), len(extra_entries)) -with open(args.reportf, 'w', newline='') as file: - fwriter = csv.writer(file) - match args.to_display: - case "found": - fwriter.writerow(["Found"]) - fwriter.writerows(map(lambda x: [x], found_entries)) - case "missing": - fwriter.writerow(["Missing"]) - fwriter.writerows(map(lambda x: [x], missing_entries)) - case "extra": - fwriter.writerow(["Extra"]) - fwriter.writerows(map(lambda x: [x], extra_entries)) - case _: - fwriter.writerow(["Found", "Missing", "Extra"]) - - found_entries = pad_list(found_entries, length) - missing_entries = pad_list(missing_entries, length) - extra_entries = pad_list(extra_entries, length) - zipped_lists = [(a, b, c) for a, b, c in zip(found_entries, - missing_entries, extra_entries)] - fwriter.writerows(zipped_lists) - - print(f"Report written to {os.getcwd()}/{args.reportf}") +if args.fout is not None: + with open(args.fout, 'w', newline='') as file: + fwriter = csv.writer(file) + match args.to_display: + case "found": + fwriter.writerow(["Found"]) + fwriter.writerows(map(lambda x: [x], found_entries)) + case "missing": + fwriter.writerow(["Missing"]) + fwriter.writerows(map(lambda x: [x], missing_entries)) + case "extra": + fwriter.writerow(["Extra"]) + fwriter.writerows(map(lambda x: [x], extra_entries)) + case _: + fwriter.writerow(["Found", "Missing", "Extra"]) + + found_entries = pad_list(found_entries, length) + missing_entries = pad_list(missing_entries, length) + extra_entries = pad_list(extra_entries, length) + zipped_lists = [(a, b, c) for a, b, c in zip(found_entries, + missing_entries, extra_entries)] + fwriter.writerows(zipped_lists) + + print(f"Report written to {os.getcwd()}/{args.fout}") From 7b1b0060b5fb742e08986f490844b8405c1aad98 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Mon, 2 Mar 2026 13:49:07 -0500 Subject: [PATCH 21/23] Flag-gate symbolics and add color to missing entries --- scripts/audit.py | 114 ++++++++++++++++++++++++++--------------------- 1 file changed, 63 insertions(+), 51 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index 0f3e95eb5..b3db33452 100755 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -38,6 +38,11 @@ "--output", dest="fout", ) +parser.add_argument( + "--include-symbolics", + action="store_true", + default=False +) parser.add_argument( "-d", "--display-results", @@ -146,7 +151,8 @@ def populate_from_dir(dir: os.DirEntry) -> set: specification.setdefault(line.removesuffix("\n"), False) print("Successfully loaded specification!") -symbolic_specification = specification.copy() +if args.include_symbolics: + symbolic_specification = 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 @@ -186,6 +192,11 @@ def populate_from_dir(dir: os.DirEntry) -> set: sub_dir_tree = populate_from_dir(entry) contents.extend(sub_dir_tree) else: + # If the name includes "-symbolic" and we don't want to include + # symbolics in our scan, skip it + if entry.name.find("-symbolic") and not args.include_symbolics: + continue + if entry.name.endswith(".svg"): if args.verbose: print(f"Adding file {entry.path} to tree") contents.append(entry.name.removesuffix(".svg")) @@ -206,76 +217,77 @@ def populate_from_dir(dir: os.DirEntry) -> set: print(f"Found {entry}") specification |= {entry: True} else: - print(f"[!!] {entry} is missing!") + print(f"\033[31m[!!] {entry} is missing!\033[0m") # Calculate percent spec coverage total_entries = len(specification.keys()) existant_color_entries = list(specification.values()).count(True) +color_results = list(specification.values()) -print(f"{existant_color_entries / total_entries * 100:.2f}% coverage of specification, color entries") +print(f"{existant_color_entries / total_entries * 100:.2f}% coverage of specification") print('-' * view_width) -# Check whether things are included in symbolic entries -for entry in symbolic_specification.keys(): - extended_entry = entry + "-symbolic" - if extended_entry in contents: - print(f"Found {extended_entry}") - symbolic_specification |= {entry: True} - else: - print(f"[!!] {extended_entry} is missing!") +if args.include_symbolics: + # Check whether things are included in symbolic entries + for entry in symbolic_specification.keys(): + extended_entry = entry + "-symbolic" + if extended_entry in contents: + print(f"Found {extended_entry}") + symbolic_specification |= {entry: True} + else: + print(f"[!!] {extended_entry} is missing!") -existant_symbolic_entries = list(symbolic_specification.values()).count(True) + existant_symbolic_entries = list(symbolic_specification.values()).count(True) -print(f"{existant_symbolic_entries / total_entries * 100:.2f}% coverage of specification, symbolic entries") + print(f"{existant_symbolic_entries / total_entries * 100:.2f}% coverage of specification, symbolic entries") -print('-' * view_width) + print('-' * view_width) -# Merging results and comparing all entries -color_results = list(specification.values()) -symbolic_results = list(symbolic_specification.values()) -results = [] -for i, value in enumerate(color_results): - results.append(symbolic_results[i] | value) -existant_entries = results.count(True) + # Merging results and comparing all entries + symbolic_results = list(symbolic_specification.values()) + results = [] + for i, value in enumerate(color_results): + results.append(symbolic_results[i] | value) -print(f"{existant_entries / total_entries * 100:.2f}% coverage of specification, all entries") + existant_entries = results.count(True) + print(f"{existant_entries / total_entries * 100:.2f}% coverage of specification, all entries") + print('-' * view_width) -print('-' * view_width) -os.chdir(script_dir) - -# Write report file with Found, Missing, and Extra information -spec_list = list(specification.keys()) -found_entries = [] -missing_entries = [] -extra_entries = [] - -# We only look at the color entry results, as symbolic is not officially in the spec -for i, result in enumerate(color_results): - # If the result is true, then it was found - # If the result is false, then it is missing - name = spec_list[i] - if result: - if args.verbose: print(f"Adding {name} to Found…") - found_entries.append(name) - else: - if args.verbose: print(f"Adding {name} to Missing…") - missing_entries.append(name) +if args.fout is not None: + os.chdir(script_dir) + + # Write report file with Found, Missing, and Extra information + spec_list = list(specification.keys()) + found_entries = [] + missing_entries = [] + extra_entries = [] + + # We only look at the color entry results, as symbolic is not officially in the spec + for i, result in enumerate(color_results): + # If the result is true, then it was found + # If the result is false, then it is missing + name = spec_list[i] + if result: + if args.verbose: print(f"Adding {name} to Found…") + found_entries.append(name) + else: + if args.verbose: print(f"Adding {name} to Missing…") + missing_entries.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 spec_list and not entry.endswith("-symbolic"): - if args.verbose: print(f"Adding {entry} to Extra") - extra_entries.append(entry) + # 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 spec_list and not entry.endswith("-symbolic"): + if args.verbose: print(f"Adding {entry} to Extra") + extra_entries.append(entry) -# Pad all lists to be the same length, then zip all three lists together -length = max(len(found_entries), len(missing_entries), len(extra_entries)) + # Pad all lists to be the same length, then zip all three lists together + length = max(len(found_entries), len(missing_entries), len(extra_entries)) -if args.fout is not None: with open(args.fout, 'w', newline='') as file: fwriter = csv.writer(file) match args.to_display: From 2fa14852ad0b571884d14576c4b3239ca194b0c1 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Mon, 2 Mar 2026 14:10:41 -0500 Subject: [PATCH 22/23] Add note on limitations with build system symlinks --- CONTRIBUTING.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c8a502a03..15114e1f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,5 +41,9 @@ that ensures all resources pulled are as current as possible. To do this: BeautifulSoup4` 5. Run the script with `python3 scripts/audit.py ` +Note that for accurate results, the fully built theme directory has to be +targeted, not the development files. Many symlinks are created by the build +system, so will not be picked up if this script is run on the development files. + ## Third-Party Brand Preservation elementary icons do not attempt to supply icons for third-party apps. Pull requests to add icons or symbolic links that would overwrite the branding of third-party apps will be rejected. From d3ae6af31f0dfa916290c59f21c128d8d1faf2f9 Mon Sep 17 00:00:00 2001 From: William Kelso Date: Fri, 6 Mar 2026 14:23:34 -0500 Subject: [PATCH 23/23] symbolics aren't part of the spec at all, they're gtk platform specific --- scripts/audit.py | 43 ++----------------------------------------- 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/scripts/audit.py b/scripts/audit.py index b3db33452..9394f9f0f 100755 --- a/scripts/audit.py +++ b/scripts/audit.py @@ -38,11 +38,6 @@ "--output", dest="fout", ) -parser.add_argument( - "--include-symbolics", - action="store_true", - default=False -) parser.add_argument( "-d", "--display-results", @@ -151,8 +146,6 @@ def populate_from_dir(dir: os.DirEntry) -> set: specification.setdefault(line.removesuffix("\n"), False) print("Successfully loaded specification!") -if args.include_symbolics: - symbolic_specification = 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 @@ -192,12 +185,8 @@ def populate_from_dir(dir: os.DirEntry) -> set: sub_dir_tree = populate_from_dir(entry) contents.extend(sub_dir_tree) else: - # If the name includes "-symbolic" and we don't want to include - # symbolics in our scan, skip it - if entry.name.find("-symbolic") and not args.include_symbolics: - continue - - if entry.name.endswith(".svg"): + if entry.name.endswith(".svg") and not + entry.name.endswith("-symbolic.svg"): if args.verbose: print(f"Adding file {entry.path} to tree") contents.append(entry.name.removesuffix(".svg")) @@ -229,34 +218,6 @@ def populate_from_dir(dir: os.DirEntry) -> set: print('-' * view_width) -if args.include_symbolics: - # Check whether things are included in symbolic entries - for entry in symbolic_specification.keys(): - extended_entry = entry + "-symbolic" - if extended_entry in contents: - print(f"Found {extended_entry}") - symbolic_specification |= {entry: True} - else: - print(f"[!!] {extended_entry} is missing!") - - existant_symbolic_entries = list(symbolic_specification.values()).count(True) - - print(f"{existant_symbolic_entries / total_entries * 100:.2f}% coverage of specification, symbolic entries") - - print('-' * view_width) - - - # Merging results and comparing all entries - symbolic_results = list(symbolic_specification.values()) - results = [] - for i, value in enumerate(color_results): - results.append(symbolic_results[i] | value) - - existant_entries = results.count(True) - - print(f"{existant_entries / total_entries * 100:.2f}% coverage of specification, all entries") - print('-' * view_width) - if args.fout is not None: os.chdir(script_dir)