diff --git a/Pyfa-Mod b/Pyfa-Mod
deleted file mode 160000
index ccebbf970..000000000
--- a/Pyfa-Mod
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit ccebbf9708861791bf53b5bfbf15cd2447966a1a
diff --git a/gui/builtinContextMenus/__init__.py b/gui/builtinContextMenus/__init__.py
index a1a26e591..809c38c0f 100644
--- a/gui/builtinContextMenus/__init__.py
+++ b/gui/builtinContextMenus/__init__.py
@@ -18,6 +18,7 @@ from gui.builtinContextMenus import resistMode
from gui.builtinContextMenus.targetProfile import editor
# Item info
from gui.builtinContextMenus import itemStats
+from gui.builtinContextMenus import fitDiff
from gui.builtinContextMenus import itemMarketJump
from gui.builtinContextMenus import fitSystemSecurity # Not really an item info but want to keep it here
from gui.builtinContextMenus import fitPilotSecurity # Not really an item info but want to keep it here
diff --git a/gui/builtinContextMenus/fitDiff.py b/gui/builtinContextMenus/fitDiff.py
new file mode 100644
index 000000000..47f1b17bc
--- /dev/null
+++ b/gui/builtinContextMenus/fitDiff.py
@@ -0,0 +1,48 @@
+# =============================================================================
+# Copyright (C) 2025
+#
+# This file is part of pyfa.
+#
+# pyfa is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# pyfa is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pyfa. If not, see .
+# =============================================================================
+
+
+# noinspection PyPackageRequirements
+import wx
+
+import gui.mainFrame
+from gui.contextMenu import ContextMenuSingle
+
+_t = wx.GetTranslation
+
+
+class FitDiff(ContextMenuSingle):
+ def __init__(self):
+ self.mainFrame = gui.mainFrame.MainFrame.getInstance()
+
+ def display(self, callingWindow, srcContext, mainItem):
+ # Only show for fittingShip context (right-click on ship)
+ return srcContext == "fittingShip"
+
+ def getText(self, callingWindow, itmContext, mainItem):
+ return _t("Fit Diff...")
+
+ def activate(self, callingWindow, fullContext, mainItem, i):
+ fitID = self.mainFrame.getActiveFit()
+ if fitID is not None:
+ from gui.fitDiffFrame import FitDiffFrame
+ FitDiffFrame(self.mainFrame, fitID)
+
+
+FitDiff.register()
diff --git a/gui/fitDiffFrame.py b/gui/fitDiffFrame.py
new file mode 100644
index 000000000..d8c2528c5
--- /dev/null
+++ b/gui/fitDiffFrame.py
@@ -0,0 +1,292 @@
+# =============================================================================
+# Copyright (C) 2025
+#
+# This file is part of pyfa.
+#
+# pyfa is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# pyfa is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with pyfa. If not, see .
+# =============================================================================
+
+
+# noinspection PyPackageRequirements
+import wx
+from collections import Counter
+
+from eos.const import FittingSlot, FittingModuleState
+from service.fit import Fit as svcFit
+from service.port.eft import exportEft, importEft, _importPrepare
+from service.const import PortEftOptions
+
+_t = wx.GetTranslation
+
+
+class FitDiffFrame(wx.Frame):
+ """A frame to display differences between two fits."""
+
+ def __init__(self, parent, fitID):
+ super().__init__(
+ parent,
+ title=_t("Fit Diff"),
+ style=wx.DEFAULT_FRAME_STYLE | wx.RESIZE_BORDER,
+ size=(1000, 600)
+ )
+ self.parent = parent
+ self.fitID = fitID
+ self.sFit = svcFit.getInstance()
+
+ # EFT export options (same as CTRL-C)
+ self.eftOptions = {
+ PortEftOptions.LOADED_CHARGES: True,
+ PortEftOptions.MUTATIONS: True,
+ PortEftOptions.IMPLANTS: True,
+ PortEftOptions.BOOSTERS: True,
+ PortEftOptions.CARGO: True,
+ }
+
+ self.initUI()
+ self.Centre()
+ self.Show()
+
+ def initUI(self):
+ panel = wx.Panel(self)
+ mainSizer = wx.BoxSizer(wx.VERTICAL)
+
+ # Instructions at the top
+ instructions = wx.StaticText(
+ panel,
+ label=_t("Paste fits in EFT format to compare")
+ )
+ mainSizer.Add(instructions, 0, wx.ALL | wx.EXPAND, 5)
+
+ # Three panes: Fit 1 | Diff | Fit 2
+ panesSizer = wx.BoxSizer(wx.HORIZONTAL)
+
+ # Pane 1: Fit 1 (editable)
+ fit1Box = wx.StaticBox(panel, label=_t("Fit 1"))
+ fit1Sizer = wx.StaticBoxSizer(fit1Box, wx.VERTICAL)
+ self.fit1Text = wx.TextCtrl(
+ panel,
+ style=wx.TE_MULTILINE | wx.TE_DONTWRAP
+ )
+ fit1Sizer.Add(self.fit1Text, 1, wx.EXPAND)
+ panesSizer.Add(fit1Sizer, 1, wx.ALL | wx.EXPAND, 5)
+
+ # Bind text changed event to update diff
+ self.fit1Text.Bind(wx.EVT_TEXT, self.onFitChanged)
+
+ # Pane 2: Diff (simple text format)
+ diffBox = wx.StaticBox(panel, label=_t("Differences"))
+ diffSizer = wx.StaticBoxSizer(diffBox, wx.VERTICAL)
+ self.diffText = wx.TextCtrl(
+ panel,
+ style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_DONTWRAP
+ )
+ diffSizer.Add(self.diffText, 1, wx.EXPAND)
+ panesSizer.Add(diffSizer, 1, wx.ALL | wx.EXPAND, 5)
+
+ # Pane 3: Fit 2 (user input)
+ fit2Box = wx.StaticBox(panel, label=_t("Fit 2"))
+ fit2Sizer = wx.StaticBoxSizer(fit2Box, wx.VERTICAL)
+ self.fit2Text = wx.TextCtrl(
+ panel,
+ style=wx.TE_MULTILINE | wx.TE_DONTWRAP
+ )
+ fit2Sizer.Add(self.fit2Text, 1, wx.EXPAND)
+
+ # Bind text changed event to update diff
+ self.fit2Text.Bind(wx.EVT_TEXT, self.onFitChanged)
+
+ panesSizer.Add(fit2Sizer, 1, wx.ALL | wx.EXPAND, 5)
+
+ mainSizer.Add(panesSizer, 1, wx.EXPAND | wx.ALL, 5)
+
+ panel.SetSizer(mainSizer)
+
+ # Load current fit into pane 1
+ self.loadFit1()
+
+ def loadFit1(self):
+ """Load the current fit into pane 1 as EFT format."""
+ fit = self.sFit.getFit(self.fitID)
+ if fit:
+ eftText = exportEft(fit, self.eftOptions, callback=None)
+ self.fit1Text.SetValue(eftText)
+
+ def onFitChanged(self, event):
+ """Handle text change in either fit pane - update diff."""
+ self.updateDiff()
+ event.Skip()
+
+ def updateDiff(self):
+ """Calculate and display the differences between the two fits."""
+ self.diffText.Clear()
+
+ fit1Text = self.fit1Text.GetValue().strip()
+ fit2Text = self.fit2Text.GetValue().strip()
+
+ if not fit1Text or not fit2Text:
+ return
+
+ # Parse both fits
+ fit1 = self.parsePastedFit(fit1Text)
+ fit2 = self.parsePastedFit(fit2Text)
+
+ if fit1 is None:
+ self.diffText.SetValue(_t("Error: Fit 1 has invalid EFT format"))
+ return
+ if fit2 is None:
+ self.diffText.SetValue(_t("Error: Fit 2 has invalid EFT format"))
+ return
+
+ # Calculate differences and format as simple text list
+ diffLines = self.calculateDiff(fit1, fit2)
+ self.diffText.SetValue('\n'.join(diffLines))
+
+ def parsePastedFit(self, text):
+ """Parse pasted EFT text into a fit object."""
+ try:
+ lines = _importPrepare(text.splitlines())
+ if not lines:
+ return None
+ return importEft(lines)
+ except Exception:
+ return None
+
+ def calculateDiff(self, fit1, fit2):
+ """Calculate items needed to transform fit1 into fit2.
+
+ Returns a list of strings in the format: "- "
+ Only shows items that need to be added (no negative values).
+ """
+ diffLines = []
+
+ # Get module counts by type for each fit (grouped by slot type)
+ fit1_modules = self.getModuleCounts(fit1)
+ fit2_modules = self.getModuleCounts(fit2)
+
+ # Diff modules - only show items needed to add
+ all_module_types = set(fit1_modules.keys()) | set(fit2_modules.keys())
+ for module_type in sorted(all_module_types):
+ count1 = fit1_modules.get(module_type, 0)
+ count2 = fit2_modules.get(module_type, 0)
+ if count2 > count1:
+ diffLines.append(f"{module_type} {count2 - count1}")
+
+ # Get drone counts
+ fit1_drones = self.getDroneCounts(fit1)
+ fit2_drones = self.getDroneCounts(fit2)
+
+ all_drone_types = set(fit1_drones.keys()) | set(fit2_drones.keys())
+ for drone_type in sorted(all_drone_types):
+ count1 = fit1_drones.get(drone_type, 0)
+ count2 = fit2_drones.get(drone_type, 0)
+ if count2 > count1:
+ diffLines.append(f"{drone_type} {count2 - count1}")
+
+ # Get fighter counts
+ fit1_fighters = self.getFighterCounts(fit1)
+ fit2_fighters = self.getFighterCounts(fit2)
+
+ all_fighter_types = set(fit1_fighters.keys()) | set(fit2_fighters.keys())
+ for fighter_type in sorted(all_fighter_types):
+ count1 = fit1_fighters.get(fighter_type, 0)
+ count2 = fit2_fighters.get(fighter_type, 0)
+ if count2 > count1:
+ diffLines.append(f"{fighter_type} {count2 - count1}")
+
+ # Get cargo counts
+ fit1_cargo = self.getCargoCounts(fit1)
+ fit2_cargo = self.getCargoCounts(fit2)
+
+ all_cargo_types = set(fit1_cargo.keys()) | set(fit2_cargo.keys())
+ for cargo_type in sorted(all_cargo_types):
+ count1 = fit1_cargo.get(cargo_type, 0)
+ count2 = fit2_cargo.get(cargo_type, 0)
+ if count2 > count1:
+ diffLines.append(f"{cargo_type} {count2 - count1}")
+
+ # Get implants
+ fit1_implants = self.getImplantNames(fit1)
+ fit2_implants = self.getImplantNames(fit2)
+
+ for implant in sorted(fit2_implants - fit1_implants):
+ diffLines.append(f"{implant} 1")
+
+ # Get boosters
+ fit1_boosters = self.getBoosterNames(fit1)
+ fit2_boosters = self.getBoosterNames(fit2)
+
+ for booster in sorted(fit2_boosters - fit1_boosters):
+ diffLines.append(f"{booster} 1")
+
+ return diffLines
+
+ def getModuleCounts(self, fit):
+ """Get a counter of module types for a fit.
+
+ Position doesn't matter, just counts by module name.
+ """
+ counts = Counter()
+ for module in fit.modules:
+ if module.isEmpty:
+ continue
+ # Use item type name for comparison
+ name = module.item.typeName if module.item else ""
+ # Add charge info if present
+ if module.charge:
+ name += f", {module.charge.typeName}"
+ # Add offline suffix if offline
+ if module.state == FittingModuleState.OFFLINE:
+ name += " /offline"
+ counts[name] += 1
+ return counts
+
+ def getDroneCounts(self, fit):
+ """Get a counter of drone types for a fit."""
+ counts = Counter()
+ for drone in fit.drones:
+ if drone.item:
+ counts[drone.item.typeName] += drone.amount
+ return counts
+
+ def getFighterCounts(self, fit):
+ """Get a counter of fighter types for a fit."""
+ counts = Counter()
+ for fighter in fit.fighters:
+ if fighter.item:
+ counts[fighter.item.typeName] += fighter.amount
+ return counts
+
+ def getCargoCounts(self, fit):
+ """Get a counter of cargo items for a fit."""
+ counts = Counter()
+ for cargo in fit.cargo:
+ if cargo.item:
+ counts[cargo.item.typeName] += cargo.amount
+ return counts
+
+ def getImplantNames(self, fit):
+ """Get a set of implant names for a fit."""
+ names = set()
+ for implant in fit.implants:
+ if implant.item:
+ names.add(implant.item.typeName)
+ return names
+
+ def getBoosterNames(self, fit):
+ """Get a set of booster names for a fit."""
+ names = set()
+ for booster in fit.boosters:
+ if booster.item:
+ names.add(booster.item.typeName)
+ return names