TL;DR
This Python app gives you a fast, point-and-click way to view and edit Autodesk Inventor file metadata (.ipt
, .iam
, .idw
, .dwg
) in bulk. It:
- Scans a folder for Inventor files
- Reads key properties (Part Number, Title, Subject, Comments, Revision, Date Checked)
- Lets you edit most fields inline in a table UI (Dear PyGui)
- Writes updates back to the native Inventor files via COM
- Auto-updates
Date Checked
when anything changes - Exports a Markdown “Part Registry” table for your notes/wiki It keeps Inventor automation safely tucked in a dedicated COM thread, so the UI stays snappy and doesn’t deadlock.
What problems this solves
- Manual property edits are slow: Opening files one by one in Inventor is painful.
- Inconsistent metadata: Revision/Title/etc. drift over time.
- Status reporting: You need a tidy Markdown table to drop into docs or Notion/Wiki. This tool puts all that into a single, friendly window.
Requirements (the “yes, it’s Windows” part)
- Windows (COM +
win32com.client
) - Autodesk Inventor installed (so COM automation works)
- Python packages:
dearpygui
(UI)pywin32
(win32com.client
+pythoncom
)tkinter
(built into most Python on Windows, used to get screen size and file dialog)
High-level architecture
- UI (Dear PyGui): Presents a table for inline editing, folder selection, and buttons (Reload, Apply Changes, Close).
- COM Worker Thread: Runs Inventor automation in its own STA (single-threaded COM apartment). The UI posts jobs to it via a thread-safe queue; results come back through
concurrent.futures.Future
. - File/Metadata Layer: Opens each file through Inventor, reads/writes property sets, and closes files cleanly.
- Markdown Export: Serializes the current in-memory rows to a nice Git-friendly table.
Configuration knobs (top of the file)
FOLDER
: Default scan location for CAD files.MD_OUT
: Where the Markdown registry gets saved.EXTS
: Which file types to include.COLUMNS
: Display order for the table columns.EDITABLE
: Which columns the user can change in the UI. Change these to fit your project structure or add more properties.
The Inventor COM helpers (read/write properties)
Opening Inventor
def open_inventor(): inv = win32.Dispatch("Inventor.Application") inv.Visible = True return inv
We spin up (or connect to) Inventor and make it visible. Visibility helps for debugging and makes COM behave a bit more predictably.
Reading properties
def get_prop(doc, set_name, prop_name): # Returns "" if the property is missing
Looks up a property inside a Property Set (e.g., "Inventor Summary Information"
, "Design Tracking Properties"
). All reads are safe: errors become empty strings so a missing property won’t crash the run.
Writing properties
def set_prop(doc, set_name, prop_name, value): # Silently no-ops on failure
Updates a property if it exists. If not, it fails quietly (by design—no hard stop in batch jobs).
Reading a file’s metadata
def read_metadata(inv, path: Path): ...
- Opens the document via Inventor (
Documents.Open
) without activating the window - Reads: Revision, Title, Subject, Part Number, Comments, Date Checked
- Normalizes
Date Checked
toYYYY-MM-DD
when possible - Always closes the document in a
finally
block
Applying updates
def apply_updates(inv, rows): ...
For each row:
- Open the document
- Compare each editable property; only write if changed
- If anything changed, set
"Date Checked"
to today (YYYY-MM-DD
) - Save and close
Data model: what is a “row”?
A row is a plain dict like:
{ "FilePath": "D:/.../part.ipt", "FileName": "part.ipt", "PartNumber": "OMG-001", "Title": "Handlebar Clamp", "Subject": "Front assembly", "Comments": "Updated for Rev B", "Revision": "B", "DateChecked": "2025-09-23", }
These power both the UI table and the Markdown export.
The UI (Dear PyGui)
Table layout
Columns are created with fixed initial widths for readability:
- FileName (read-only)
- PartNumber (editable)
- Title (editable)
- Subject (editable)
- Comments (editable)
- Revision (editable via a button)
- DateChecked (read-only, auto-managed; shown as a button for right alignment)
Why buttons for
Revision
andDateChecked
?
Because Dear PyGui makes it easy to right-align button text with a custom theme. For consistency, both editable and read-only cells in those columns render via buttons bound to aright_align_theme
. ForRevision
, edits are handled through inline entry (button click currently just shows a status hint that it’s auto-managed; the actual value is updated by Apply).
Inline editing behavior
- For text fields (PartNumber, Title, Subject, Comments), edits happen in place.
- Status line updates tell you what changed.
Revision
andDateChecked
are considered auto-managed:Revision
is editable in the table via its input (the button is just a UI affordance here).Date Checked
is set automatically when any property is changed during Apply.
Folder selection & reload
- “Browse” opens a folder dialog; “Reload” scans and repopulates the table by asking the COM worker to
load_rows
.
Apply & Save
- Apply Changes:
- Sends the current in-memory rows to the COM worker, which writes changes to each file.
- Reloads from disk (to show the final on-disk truth).
- Automatically exports Markdown to
MD_OUT
.
- Close: Ends the Dear PyGui loop and triggers cleanup.
The COM worker (why a separate thread?)
COM + GUI on the same thread is a recipe for deadlocks and “Application Not Responding” grief. So we:
- Start a daemon thread (
COMWorker
) - Initialize COM explicitly with
pythoncom.CoInitialize()
inside that thread - Create/open the Inventor Application once there
- Exchange work via a
queue.Queue()
of(op, args, future)
- Support two ops:
"load_rows"
→ scans folder and reads metadata"apply_updates"
→ writes changes back to files
- Cleanly
Quit()
Inventor andCoUninitialize()
on shutdown The UI thread never touches COM directly—only the worker does. This separation keeps the app responsive and COM-correct.
Markdown export
def export_markdown(rows, md_path): ...
- Creates parent folder if needed
- Escapes pipes and newlines so the table renders correctly
- Writes a simple Git-friendly table you can paste into documentation
Example header:
| File Name | Part Number | Title | Subject | Comments | Revision | Date Checked | |---|---|---|---|---|---|---|
Startup & window sizing niceties
- Uses
tkinter
to detect screen size and position the window center-screen at 50% of width/height (comment says 80%, code currently returns 50%). - Removes window chrome (
no_title_bar=True
,no_resize=True
,no_move=True
) for a “panel” feel. - Custom theme aligns Revision and Date Checked columns to the right with transparent buttons.
Note: The docstring mentions “Get 80% of screen size” but the code multiplies by
0.5
. Adjust if you truly want 80%.
Data flow (end-to-end)
- Launch app → Start COM worker → Optionally preload default
FOLDER
. - Reload → Worker reads files → UI table refresh.
- Edit inline → Rows mutate in memory.
- Apply Changes → Worker writes to Inventor, updates
Date Checked
, saves → Reload from disk → Export Markdown. - Close → Clean shutdown of worker + COM.
Error handling philosophy
- Gentle failures:
get_prop
/set_prop
swallow most property errors so a missing field doesn’t nuke your batch. - Always close documents:
finally
blocks try hard toClose(True)
to avoid file locks. - Status line: Human-readable feedback for most user operations.
- Worker exceptions: Surface via
Future
results; user sees a concise message in the status bar.
Customization tips
- Add a new column/property
- Add the name to
COLUMNS
. - If you want it editable, add to
EDITABLE
. - Update
read_metadata
to fetch it (pick the right Property Set + name). - Update
apply_updates
to write it back when changed. - (Optional) Add to Markdown export order.
- Add the name to
- Change file types
- Tweak
EXTS
to include/remove extensions.
- Tweak
- Default folder & output
- Set
FOLDER
andMD_OUT
for your project structure.
- Set
- Alignment / Widths
- Adjust
init_width_or_weight
per column to fit your data.
- Adjust
Known limitations / gotchas
- Inventor must be installed and licensed on the machine running this.
- Property existence: Some files might lack a property or set—writes are best-effort and silent on failure.
- Date parsing:
Date Checked
is normalized if it looks likeYYYY-MM-DD ...
; otherwise the raw value is kept. - UI editing for Revision: The right-aligned button itself is not the input; the input is the adjacent field. The button click simply informs that the field is auto-managed. (You can convert this to a true input if you prefer.)
- Performance: Very large folders mean lots of COM open/close cycles; consider filtering or batching.
Quickstart
- Install deps:
pip install dearpygui pywin32
- Set
FOLDER
andMD_OUT
at the top of the script. - Run:
python inventor_properties_editor.py
- Click Reload to load files.
- Edit fields inline.
- Hit Apply Changes. Your files update and the Markdown registry is written.
FAQ
Q: Why not edit properties without opening each file?
A: Inventor exposes properties through its COM API on opened documents. We open non-visibly and close fast, which is the supported route.
Q: Can I keep Inventor hidden?
A: You can toggle inv.Visible = False
, but visible is handy for troubleshooting and sometimes stabilizes COM.
Q: Can I add custom iProperties?
A: Yes—extend read_metadata
/ apply_updates
with the correct Property Set and name. Common sets:
"Inventor Summary Information"
"Design Tracking Properties"
- Your custom set names, if present
Q: Can I prevent auto-updating Date Checked?
A: Remove or tweak theproperties_changed
logic inapply_updates
.
Future nice-to-haves
- Filter/search bar for large assemblies
- Progress bar during Apply
- Multi-folder support and recursion
- CSV export/import
- Diff preview (highlight what will change before applying)
Final notes
The heart of this script is separating UI from COM. By letting the COM worker own Inventor and treating UI edits as row diffs, you get a smooth editor that plays nicely with Inventor’s threading model. The Markdown export is the cherry on top for documentation hygiene.
Full script
import os
from pathlib import Path
from datetime import datetime
import dearpygui.dearpygui as dpg
import win32com.client as win32
import pythoncom
from threading import local, Thread
from concurrent.futures import Future
import queue
import tkinter as tk
# --- CONFIG ---
FOLDER = r"D:\OneDrive\001. PROJECTEN\000. KUBUZ\OPENFRAME\CAD"
MD_OUT = r"D:\OneDrive\005. NOTES\OpenFrame\Part Registry.md"
EXTS = {".ipt", ".iam", ".idw", ".dwg"}
COLUMNS = ["FileName", "PartNumber", "Title", "Subject", "Comments", "Revision", "DateChecked"]
EDITABLE = {"PartNumber", "Title", "Subject", "Comments", "Revision"} # which columns can be edited
# --- Inventor helpers ---
def open_inventor():
inv = win32.Dispatch("Inventor.Application")
inv.Visible = True
return inv
def get_prop(doc, set_name, prop_name):
try:
return str(doc.PropertySets.Item(set_name).Item(prop_name).Value)
except:
return ""
def set_prop(doc, set_name, prop_name, value):
try:
doc.PropertySets.Item(set_name).Item(prop_name).Value = value
except:
pass
def read_metadata(inv, path: Path):
doc = None
try:
doc = inv.Documents.Open(str(path), False)
rev = get_prop(doc, "Inventor Summary Information", "Revision Number")
title = get_prop(doc, "Inventor Summary Information", "Title")
subject = get_prop(doc, "Inventor Summary Information", "Subject")
partnum = get_prop(doc, "Design Tracking Properties", "Part Number")
comments = get_prop(doc, "Inventor Summary Information", "Comments")
date_checked_raw = get_prop(doc, "Design Tracking Properties", "Date Checked")
# Format date to YYYY-MM-DD only
try:
if date_checked_raw:
# Try to parse and reformat the date
date_obj = datetime.strptime(date_checked_raw.split()[0], "%Y-%m-%d")
date_checked = date_obj.strftime("%Y-%m-%d")
else:
date_checked = ""
except:
# If parsing fails, try to extract just the date part
if date_checked_raw and " " in date_checked_raw:
date_checked = date_checked_raw.split()[0]
else:
date_checked = date_checked_raw or ""
return {
"FilePath": str(path),
"FileName": path.name,
"PartNumber": partnum,
"Title": title,
"Subject": subject,
"Comments": comments,
"Revision": rev,
"DateChecked": date_checked,
}
finally:
try:
if doc is not None: doc.Close(True)
except:
pass
def apply_updates(inv, rows):
current_date = datetime.now().strftime("%Y-%m-%d")
for r in rows:
p = Path(r["FilePath"])
if not p.exists():
continue
doc = None
try:
doc = inv.Documents.Open(str(p), False)
# Track if any properties changed to update Date Checked
properties_changed = False
# Props
if r["Revision"] != get_prop(doc, "Inventor Summary Information", "Revision Number"):
set_prop(doc, "Inventor Summary Information", "Revision Number", r["Revision"])
properties_changed = True
if r["Title"] != get_prop(doc, "Inventor Summary Information", "Title"):
set_prop(doc, "Inventor Summary Information", "Title", r["Title"])
properties_changed = True
if r["Subject"] != get_prop(doc, "Inventor Summary Information", "Subject"):
set_prop(doc, "Inventor Summary Information", "Subject", r["Subject"])
properties_changed = True
if r["PartNumber"] != get_prop(doc, "Design Tracking Properties", "Part Number"):
set_prop(doc, "Design Tracking Properties", "Part Number", r["PartNumber"])
properties_changed = True
if r["Comments"] != get_prop(doc, "Inventor Summary Information", "Comments"):
set_prop(doc, "Inventor Summary Information", "Comments", r["Comments"])
properties_changed = True
# Update Date Checked if any properties changed
if properties_changed:
set_prop(doc, "Design Tracking Properties", "Date Checked", current_date)
doc.Save()
finally:
try:
if doc is not None: doc.Close(True)
except:
pass
def export_markdown(rows, md_path):
os.makedirs(Path(md_path).parent, exist_ok=True)
def esc(s):
s = "" if s is None else str(s)
return s.replace("|", "\\|").replace("\r", " ").replace("\n", " ")
lines = []
lines.append("| File Name | Part Number | Title | Subject | Comments | Revision | Date Checked |")
lines.append("|---|---|---|---|---|---|---|")
for r in rows:
lines.append("| " + " | ".join([
esc(r["FileName"]),
esc(r["PartNumber"]),
esc(r["Title"]),
esc(r["Subject"]),
esc(r["Comments"]),
esc(r["Revision"]),
esc(r["DateChecked"]),
]) + " |")
Path(md_path).write_text("\n".join(lines), encoding="utf-8")
# --- Data ---
def list_inv_files(folder):
return [p for p in Path(folder).glob("*") if p.is_file() and p.suffix.lower() in EXTS]
def load_rows(inv, folder):
files = list_inv_files(folder)
return [read_metadata(inv, p) for p in files]
# --- DPG UI Helpers ---
DPG_TAGS = {
"folder_input": "-FOLDER-",
"folder_dialog": "-FOLDER_DIALOG-",
"table": "-TABLE-",
"status": "-STATUS-",
"edit_FileName": "-FileName-",
"edit_PartNumber": "-PartNumber-",
"edit_Title": "-Title-",
"edit_Subject": "-Subject-",
"edit_Comments": "-Comments-",
"edit_Revision": "-Revision-",
"edit_DateChecked": "-DateChecked-",
}
_rows_state = {
"rows": [],
"selected_index": None,
"worker": None,
}
_thread_local = local()
def _ensure_com_initialized():
try:
if not getattr(_thread_local, "com_initialized", False):
pythoncom.CoInitialize()
_thread_local.com_initialized = True
except Exception:
pass
class COMWorker:
def __init__(self):
self._q = queue.Queue()
self._thread = Thread(target=self._run, daemon=True)
self._started = False
def start(self):
if not self._started:
self._thread.start()
self._started = True
def call(self, op, *args):
fut = Future()
self._q.put((op, args, fut))
return fut.result()
def stop(self):
fut = Future()
try:
self._q.put(("_stop", (), fut))
fut.result(timeout=5)
except Exception:
pass
def _run(self):
inv = None
pythoncom.CoInitialize()
try:
inv = open_inventor()
while True:
op, args, fut = self._q.get()
if op == "_stop":
try:
fut.set_result(True)
except Exception:
pass
break
try:
if op == "load_rows":
folder, = args
res = load_rows(inv, folder)
fut.set_result(res)
elif op == "apply_updates":
rows, = args
apply_updates(inv, rows)
fut.set_result(True)
else:
fut.set_exception(RuntimeError(f"Unknown op: {op}"))
except Exception as ex:
try:
fut.set_exception(ex)
except Exception:
pass
except Exception:
# Swallow worker-level failures; they will surface via Future exceptions
pass
finally:
try:
if inv is not None:
inv.Quit()
except Exception:
pass
try:
pythoncom.CoUninitialize()
except Exception:
pass
def _set_status(text):
try:
dpg.set_value(DPG_TAGS["status"], text)
except Exception:
pass
def _populate_edit_fields(row_dict):
dpg.set_value(DPG_TAGS["edit_FileName"], row_dict.get("FileName", ""))
for c in EDITABLE:
dpg.set_value(DPG_TAGS[f"edit_{c}"], row_dict.get(c, ""))
def _get_edit_values(base_row):
updated = dict(base_row)
for c in EDITABLE:
updated[c] = dpg.get_value(DPG_TAGS[f"edit_{c}"]) or base_row.get(c, "")
return updated
def _rebuild_table():
table_tag = DPG_TAGS["table"]
# clear existing rows
children = dpg.get_item_children(table_tag, 1) or []
for child in children:
dpg.delete_item(child)
# add rows
for idx, r in enumerate(_rows_state["rows"]):
with dpg.table_row(parent=table_tag):
# FileName (read-only)
dpg.add_text(r.get("FileName", ""))
# Other columns: make editable where allowed
for c in COLUMNS[1:]:
if c in EDITABLE:
# Right align Revision and DateChecked columns using buttons
if c in ["Revision", "DateChecked"]:
# Use button with transparent background for right alignment
dpg.add_button(label=r.get(c, ""), width=-1, callback=_on_cell_edit, user_data=(idx, c))
dpg.bind_item_theme(dpg.last_item(), "right_align_theme")
else:
dpg.add_input_text(default_value=r.get(c, ""), callback=_on_cell_edit, user_data=(idx, c), width=-1)
else:
# Right align Revision and DateChecked columns for read-only text too
if c in ["Revision", "DateChecked"]:
# Use button with transparent background for right alignment
dpg.add_button(label=r.get(c, ""), width=-1)
dpg.bind_item_theme(dpg.last_item(), "right_align_theme")
else:
dpg.add_text(r.get(c, ""))
def _on_row_select(sender, app_data, user_data):
try:
idx = int(user_data)
except Exception:
return
if idx < 0 or idx >= len(_rows_state["rows"]):
return
_rows_state["selected_index"] = idx
_populate_edit_fields(_rows_state["rows"][idx])
_set_status(f"Selected row {idx+1} of {len(_rows_state['rows'])}")
def _on_cell_edit(sender, app_data, user_data):
# For buttons, app_data is the button label (current value)
# For input text, app_data is the new text value
try:
idx, col = user_data
except Exception:
return
if idx < 0 or idx >= len(_rows_state["rows"]):
return
if col not in EDITABLE:
return
# If it's a button (Revision or DateChecked), we need to handle it differently
if col in ["Revision", "DateChecked"]:
# For now, just show a message that these fields are auto-managed
_set_status(f"Row {idx+1} - {col} is auto-managed")
else:
# For input text fields
_rows_state["rows"][idx][col] = app_data or ""
_set_status(f"Edited row {idx+1} - {col}")
def _on_browse_folder():
dpg.configure_item(DPG_TAGS["folder_dialog"], show=True)
def _on_folder_chosen(sender, app_data):
# app_data contains selections dict: {"file_path_name": path}
try:
folder = app_data.get("file_path_name", "")
except Exception:
folder = ""
if folder:
dpg.set_value(DPG_TAGS["folder_input"], folder)
def _on_reload():
folder = dpg.get_value(DPG_TAGS["folder_input"]) or FOLDER
if not Path(folder).exists():
_set_status(f"Folder not found: {folder}")
return
try:
rows = _rows_state["worker"].call("load_rows", folder)
_rows_state["rows"] = rows
_rows_state["selected_index"] = None
_rebuild_table()
_set_status(f"Loaded {len(_rows_state['rows'])} files from {folder}")
except Exception as ex:
_set_status(f"Reload failed: {ex}")
def _on_update_row():
# No longer needed with inline editing, keep as no-op for compatibility
_set_status("Rows are edited directly in the table.")
def _on_apply_changes():
try:
_rows_state["worker"].call("apply_updates", _rows_state["rows"])
# Refresh rows from disk
folder = dpg.get_value(DPG_TAGS["folder_input"]) or FOLDER
_rows_state["rows"] = _rows_state["worker"].call("load_rows", folder)
_rows_state["selected_index"] = None
_rebuild_table()
# Automatically save markdown after applying changes
try:
export_markdown(_rows_state["rows"], MD_OUT)
_set_status("Changes applied to Inventor files and markdown saved.")
except Exception as ex:
_set_status(f"Changes applied to Inventor files, but markdown save failed: {ex}")
except Exception as ex:
_set_status(f"Apply failed: {ex}")
def _on_save_markdown():
try:
export_markdown(_rows_state["rows"], MD_OUT)
_set_status(f"Markdown saved: {MD_OUT}")
except Exception as ex:
_set_status(f"Save Markdown failed: {ex}")
def _on_exit():
try:
if _rows_state.get("worker") is not None:
_rows_state["worker"].stop()
except Exception:
pass
try:
pythoncom.CoUninitialize()
except Exception:
pass
def get_screen_size():
"""Get screen dimensions and return 80% of screen size"""
root = tk.Tk()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
root.destroy()
# Return 50% of screen size
return int(screen_width * 0.5), int(screen_height * 0.5)
def center_window(window_width, window_height):
"""Calculate position to center window on screen"""
root = tk.Tk()
screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()
root.destroy()
# Calculate center position
x = (screen_width - window_width) // 2
y = (screen_height - window_height) // 2
return x, y
def main():
# Start COM worker thread that owns Inventor COM apartment
worker = COMWorker()
worker.start()
_rows_state["worker"] = worker
# Preload
if Path(FOLDER).exists():
try:
_rows_state["rows"] = worker.call("load_rows", FOLDER)
except Exception as ex:
_set_status(f"Initial load failed: {ex}")
else:
_set_status(f"Folder not found: {FOLDER}")
dpg.create_context()
# Create theme for right-aligned buttons
with dpg.theme(tag="right_align_theme"):
with dpg.theme_component(dpg.mvButton):
dpg.add_theme_style(dpg.mvStyleVar_ButtonTextAlign, 1.0, 0.5) # Right-align text
dpg.add_theme_color(dpg.mvThemeCol_Button, (0, 0, 0, 0)) # Transparent background
dpg.add_theme_color(dpg.mvThemeCol_ButtonActive, (0, 0, 0, 0))
dpg.add_theme_color(dpg.mvThemeCol_ButtonHovered, (0, 0, 0, 0))
# Get 80% of screen size and center position
window_width, window_height = get_screen_size()
window_x, window_y = center_window(window_width, window_height)
with dpg.window(label="Inventor Properties Editor", width=window_width, height=window_height, no_collapse=True, no_title_bar=True, no_resize=True, no_move=True) as main_window:
with dpg.group(horizontal=True):
dpg.add_text("Folder:")
dpg.add_input_text(tag=DPG_TAGS["folder_input"], default_value=FOLDER, width=700)
dpg.add_button(label="Browse", callback=_on_browse_folder)
dpg.add_button(label="Reload", callback=_on_reload)
dpg.add_spacer(height=5)
with dpg.table(tag=DPG_TAGS["table"], header_row=True, borders_innerH=True, borders_innerV=True, borders_outerH=True, borders_outerV=True, resizable=True, policy=dpg.mvTable_SizingFixedFit, scrollY=True, height=600):
# columns with specific widths
dpg.add_table_column(label="FileName", init_width_or_weight=150)
dpg.add_table_column(label="PartNumber", init_width_or_weight=150)
dpg.add_table_column(label="Title", init_width_or_weight=300)
dpg.add_table_column(label="Subject", init_width_or_weight=200)
dpg.add_table_column(label="Comments", init_width_or_weight=100)
dpg.add_table_column(label="Revision", init_width_or_weight=70)
dpg.add_table_column(label="DateChecked", init_width_or_weight=130)
dpg.add_spacer(height=5)
with dpg.group(horizontal=True):
dpg.add_button(label="Apply Changes", callback=_on_apply_changes)
dpg.add_button(label="Close", callback=lambda: dpg.stop_dearpygui())
dpg.add_spacer(height=5)
dpg.add_text("", tag=DPG_TAGS["status"]) # status line
# Folder dialog
with dpg.file_dialog(directory_selector=True, show=False, callback=_on_folder_chosen, tag=DPG_TAGS["folder_dialog"], width=700, height=400):
dpg.add_file_extension(".*", color=(255, 255, 255, 255))
dpg.create_viewport(title="Inventor Properties Editor", width=window_width, height=window_height, x_pos=window_x, y_pos=window_y)
dpg.setup_dearpygui()
dpg.set_exit_callback(_on_exit)
dpg.show_viewport()
dpg.set_primary_window(main_window, True)
# Populate table and clear edit fields
_rebuild_table()
if _rows_state["rows"]:
_set_status(f"Loaded {len(_rows_state['rows'])} files from {FOLDER}")
dpg.start_dearpygui()
dpg.destroy_context()
if __name__ == "__main__":
main()