Add git self-update feature to all tools and enhance DOS Setup New shared modul...

File: gamecom-downloader/gamecom_downloader.py
     GloomyBackground, draw_nerdymark_brand, draw_title_with_glow, create_panel,
     get_theme, VOID_BLACK, DEEP_GRAY, SMOKE_GRAY, MIST_GRAY, PALE_GRAY, FOG_WHITE
 )
+from git_update import UpdateChecker
 BASE_URL = "https://myrient.erista.me/files/No-Intro/Tiger%20-%20Game.com/"
 ROM_DIR = "/run/media/deck/SK256/Emulation/roms/gamecom"
 def __init__(self):
         self.font_brand = pygame.font.Font(None, 24)
         self.background = GloomyBackground(self.width, self.height, accent_color=ACCENT)
         self.clock = pygame.time.Clock()
+        # Git update checker
+        self.update_checker = UpdateChecker()
+        self.update_banner_visible = False
     def get_existing_games(self):
         existing = set()
 def draw_main_menu(self):
         controls_surf = self.font_small.render(controls, True, MIST_GRAY)
         self.screen.blit(controls_surf, (self.width//2 - controls_surf.get_width()//2, self.height - 40))
         draw_nerdymark_brand(self.screen, self.font_brand, ACCENT_DIM)
+        self.draw_update_banner()
     def draw_keyboard(self):
         overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
 def download_game(self, game_name, href):
         self.filter_games()
         return True
+    def draw_update_banner(self):
+        """Draw update available notification banner at top of screen"""
+        if not self.update_banner_visible:
+            return
+        banner_height = 30
+        banner_surface = pygame.Surface((self.width, banner_height), pygame.SRCALPHA)
+        pygame.draw.rect(banner_surface, (80, 60, 20, 200), (0, 0, self.width, banner_height))
+        msg = self.update_checker.message
+        if self.update_checker.can_update:
+            msg += " - Press SELECT to update"
+        text_surf = self.font_tiny.render(msg, True, (255, 220, 100))
+        banner_surface.blit(text_surf, (self.width//2 - text_surf.get_width()//2, 6))
+        self.screen.blit(banner_surface, (0, 0))
+
+    def check_for_updates(self):
+        """Check for updates and show dialog if update can be applied"""
+        self.draw_message("Checking for updates...", "Please wait")
+        pygame.display.flip()
+
+        status = self.update_checker.check()
+        if status['update_available']:
+            self.update_banner_visible = True
+            if status['can_update']:
+                return self.offer_update_dialog()
+        return False
+
+    def offer_update_dialog(self):
+        """Show dialog offering to update. Returns True if user chose to update."""
+        selected = 0
+        options = ["Update Now", "Skip"]
+
+        while True:
+            self.background.update()
+            self.background.draw(self.screen)
+            draw_title_with_glow(self.screen, self.font_large, "UPDATE AVAILABLE", ACCENT, self.height//2 - 100)
+
+            msg = f"{self.update_checker._status['behind']} new commits available"
+            msg_surf = self.font_medium.render(msg, True, PALE_GRAY)
+            self.screen.blit(msg_surf, (self.width//2 - msg_surf.get_width()//2, self.height//2 - 40))
+
+            for i, opt in enumerate(options):
+                y = self.height//2 + 20 + i * 50
+                color = ACCENT if i == selected else MIST_GRAY
+                opt_surf = self.font_medium.render(f"{'> ' if i == selected else '  '}{opt}", True, color)
+                self.screen.blit(opt_surf, (self.width//2 - opt_surf.get_width()//2, y))
+
+            draw_nerdymark_brand(self.screen, self.font_brand, ACCENT_DIM)
+            pygame.display.flip()
+
+            for event in pygame.event.get():
+                if event.type == pygame.QUIT:
+                    return False
+                if event.type == pygame.KEYDOWN:
+                    if event.key == pygame.K_UP:
+                        selected = max(0, selected - 1)
+                    elif event.key == pygame.K_DOWN:
+                        selected = min(len(options) - 1, selected + 1)
+                    elif event.key == pygame.K_RETURN:
+                        if selected == 0:
+                            return self.perform_update()
+                        return False
+                    elif event.key == pygame.K_ESCAPE:
+                        return False
+                if event.type == pygame.JOYBUTTONDOWN:
+                    if event.button == 0:
+                        if selected == 0:
+                            return self.perform_update()
+                        return False
+                    elif event.button == 1:
+                        return False
+
+            if self.joystick and self.joystick.get_numhats() > 0:
+                hat = self.joystick.get_hat(0)
+                if hat[1] == 1:
+                    selected = max(0, selected - 1)
+                    pygame.time.wait(150)
+                elif hat[1] == -1:
+                    selected = min(len(options) - 1, selected + 1)
+                    pygame.time.wait(150)
+
+            self.clock.tick(30)
+
+    def perform_update(self):
+        """Perform the git update and show result"""
+        self.draw_message("Updating...", "Pulling latest changes from git")
+        pygame.display.flip()
+
+        success, message = self.update_checker.update()
+
+        if success:
+            self.draw_message("Update Complete!", "Please restart the tool")
+            pygame.display.flip()
+            pygame.time.wait(3000)
+            pygame.quit()
+            sys.exit(0)
+        else:
+            self.draw_message("Update Failed", message[:50])
+            pygame.display.flip()
+            pygame.time.wait(3000)
+            return False
+
+
     def download_all(self):
         missing_games = [(name, href) for name, href, status in self.filtered_games if not status]
         if not missing_games:
 def download_all(self):
     def run(self):
         os.makedirs(ROM_DIR, exist_ok=True)
+
+        # Check for updates at startup
+        self.check_for_updates()
         self.existing_games = self.get_existing_games()
         self.games = self.fetch_game_list()
File: gamecube-downloader/gamecube_downloader.py
     GloomyBackground, draw_nerdymark_brand, draw_title_with_glow, create_panel,
     get_theme, VOID_BLACK, DEEP_GRAY, SMOKE_GRAY, MIST_GRAY, PALE_GRAY, FOG_WHITE
 )
+from git_update import UpdateChecker
 BASE_URL = "https://myrient.erista.me/files/Redump/Nintendo%20-%20GameCube%20-%20NKit%20RVZ%20%5Bzstd-19-128k%5D/"
 GC_ROM_DIR = "/run/media/deck/SK256/Emulation/roms/gc"
 def __init__(self):
         self.background = GloomyBackground(self.width, self.height, accent_color=ACCENT)
         self.clock = pygame.time.Clock()
+        # Git update checker
+        self.update_checker = UpdateChecker()
+        self.update_banner_visible = False
     def get_existing_games(self):
         """Get list of already downloaded games"""
 def draw_main_menu(self):
         self.screen.blit(controls_surf, (self.width//2 - controls_surf.get_width()//2, self.height - 40))
         draw_nerdymark_brand(self.screen, self.font_brand, ACCENT_DIM)
+        self.draw_update_banner()
     def draw_keyboard(self):
         overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
 def download_game(self, game_name, href):
         self.filter_games()
         return True
+    def draw_update_banner(self):
+        """Draw update available notification banner at top of screen"""
+        if not self.update_banner_visible:
+            return
+        banner_height = 30
+        banner_surface = pygame.Surface((self.width, banner_height), pygame.SRCALPHA)
+        pygame.draw.rect(banner_surface, (80, 60, 20, 200), (0, 0, self.width, banner_height))
+        msg = self.update_checker.message
+        if self.update_checker.can_update:
+            msg += " - Press SELECT to update"
+        text_surf = self.font_tiny.render(msg, True, (255, 220, 100))
+        banner_surface.blit(text_surf, (self.width//2 - text_surf.get_width()//2, 6))
+        self.screen.blit(banner_surface, (0, 0))
+
+    def check_for_updates(self):
+        """Check for updates and show dialog if update can be applied"""
+        self.draw_message("Checking for updates...", "Please wait")
+        pygame.display.flip()
+
+        status = self.update_checker.check()
+        if status['update_available']:
+            self.update_banner_visible = True
+            if status['can_update']:
+                return self.offer_update_dialog()
+        return False
+
+    def offer_update_dialog(self):
+        """Show dialog offering to update. Returns True if user chose to update."""
+        selected = 0
+        options = ["Update Now", "Skip"]
+
+        while True:
+            self.background.update()
+            self.background.draw(self.screen)
+            draw_title_with_glow(self.screen, self.font_large, "UPDATE AVAILABLE", ACCENT, self.height//2 - 100)
+
+            msg = f"{self.update_checker._status['behind']} new commits available"
+            msg_surf = self.font_medium.render(msg, True, PALE_GRAY)
+            self.screen.blit(msg_surf, (self.width//2 - msg_surf.get_width()//2, self.height//2 - 40))
+
+            for i, opt in enumerate(options):
+                y = self.height//2 + 20 + i * 50
+                color = ACCENT if i == selected else MIST_GRAY
+                opt_surf = self.font_medium.render(f"{'> ' if i == selected else '  '}{opt}", True, color)
+                self.screen.blit(opt_surf, (self.width//2 - opt_surf.get_width()//2, y))
+
+            draw_nerdymark_brand(self.screen, self.font_brand, ACCENT_DIM)
+            pygame.display.flip()
+
+            for event in pygame.event.get():
+                if event.type == pygame.QUIT:
+                    return False
+                if event.type == pygame.KEYDOWN:
+                    if event.key == pygame.K_UP:
+                        selected = max(0, selected - 1)
+                    elif event.key == pygame.K_DOWN:
+                        selected = min(len(options) - 1, selected + 1)
+                    elif event.key == pygame.K_RETURN:
+                        if selected == 0:
+                            return self.perform_update()
+                        return False
+                    elif event.key == pygame.K_ESCAPE:
+                        return False
+                if event.type == pygame.JOYBUTTONDOWN:
+                    if event.button == 0:
+                        if selected == 0:
+                            return self.perform_update()
+                        return False
+                    elif event.button == 1:
+                        return False
+
+            if self.joystick and self.joystick.get_numhats() > 0:
+                hat = self.joystick.get_hat(0)
+                if hat[1] == 1:
+                    selected = max(0, selected - 1)
+                    pygame.time.wait(150)
+                elif hat[1] == -1:
+                    selected = min(len(options) - 1, selected + 1)
+                    pygame.time.wait(150)
+
+            self.clock.tick(30)
+
+    def perform_update(self):
+        """Perform the git update and show result"""
+        self.draw_message("Updating...", "Pulling latest changes from git")
+        pygame.display.flip()
+
+        success, message = self.update_checker.update()
+
+        if success:
+            self.draw_message("Update Complete!", "Please restart the tool")
+            pygame.display.flip()
+            pygame.time.wait(3000)
+            pygame.quit()
+            sys.exit(0)
+        else:
+            self.draw_message("Update Failed", message[:50])
+            pygame.display.flip()
+            pygame.time.wait(3000)
+            return False
+
+
     def download_all(self):
         """Download all missing games from the filtered list"""
         missing_games = [(name, href) for name, href, status in self.filtered_games if not status]
 def download_all(self):
     def run(self):
         os.makedirs(GC_ROM_DIR, exist_ok=True)
+        # Check for updates at startup
+        self.check_for_updates()
+
         self.existing_games = self.get_existing_games()
         self.games = self.fetch_game_list()
File: jaguar-downloader/jaguar_downloader.py
     GloomyBackground, draw_nerdymark_brand, draw_title_with_glow, create_panel,
     get_theme, VOID_BLACK, DEEP_GRAY, SMOKE_GRAY, MIST_GRAY, PALE_GRAY, FOG_WHITE
 )
+from git_update import UpdateChecker
 BASE_URL = "https://myrient.erista.me/files/No-Intro/Atari%20-%20Atari%20Jaguar%20%28J64%29/"
 ROM_DIR = "/run/media/deck/SK256/Emulation/roms/atarijaguar"
 def __init__(self):
         self.font_brand = pygame.font.Font(None, 24)
         self.background = GloomyBackground(self.width, self.height, accent_color=ACCENT)
         self.clock = pygame.time.Clock()
+        # Git update checker
+        self.update_checker = UpdateChecker()
+        self.update_banner_visible = False
     def get_existing_games(self):
         existing = set()
 def draw_main_menu(self):
         controls_surf = self.font_small.render(controls, True, MIST_GRAY)
         self.screen.blit(controls_surf, (self.width//2 - controls_surf.get_width()//2, self.height - 40))
         draw_nerdymark_brand(self.screen, self.font_brand, ACCENT_DIM)
+        self.draw_update_banner()
     def draw_keyboard(self):
         overlay = pygame.Surface((self.width, self.height), pygame.SRCALPHA)
 def download_game(self, game_name, href):
         self.filter_games()
         return True
+    def draw_update_banner(self):
+        """Draw update available notification banner at top of screen"""
+        if not self.update_banner_visible:
+            return
+        banner_height = 30
+        banner_surface = pygame.Surface((self.width, banner_height), pygame.SRCALPHA)
+        pygame.draw.rect(banner_surface, (80, 60, 20, 200), (0, 0, self.width, banner_height))
+        msg = self.update_checker.message
+        if self.update_checker.can_update:
+            msg += " - Press SELECT to update"
+        text_surf = self.font_tiny.render(msg, True, (255, 220, 100))
+        banner_surface.blit(text_surf, (self.width//2 - text_surf.get_width()//2, 6))
+        self.screen.blit(banner_surface, (0, 0))
+
+    def check_for_updates(self):
+        """Check for updates and show dialog if update can be applied"""
+        self.draw_message("Checking for updates...", "Please wait")
+        pygame.display.flip()
+
+        status = self.update_checker.check()
+        if status['update_available']:
+            self.update_banner_visible = True
+            if status['can_update']:
+                return self.offer_update_dialog()
+        return False
+
+    def offer_update_dialog(self):
+        """Show dialog offering to update. Returns True if user chose to update."""
+        selected = 0
+        options = ["Update Now", "Skip"]
+
+        while True:
+            self.background.update()
+            self.background.draw(self.screen)
+            draw_title_with_glow(self.screen, self.font_large, "UPDATE AVAILABLE", ACCENT, self.height//2 - 100)
+
+            msg = f"{self.update_checker._status['behind']} new commits available"
+            msg_surf = self.font_medium.render(msg, True, PALE_GRAY)
+            self.screen.blit(msg_surf, (self.width//2 - msg_surf.get_width()//2, self.height//2 - 40))
+
+            for i, opt in enumerate(options):
+                y = self.height//2 + 20 + i * 50
+                color = ACCENT if i == selected else MIST_GRAY
+                opt_surf = self.font_medium.render(f"{'> ' if i == selected else '  '}{opt}", True, color)
+                self.screen.blit(opt_surf, (self.width//2 - opt_surf.get_width()//2, y))
+
+            draw_nerdymark_brand(self.screen, self.font_brand, ACCENT_DIM)
+            pygame.display.flip()
+
+            for event in pygame.event.get():
+                if event.type == pygame.QUIT:
+                    return False
+                if event.type == pygame.KEYDOWN:
+                    if event.key == pygame.K_UP:
+                        selected = max(0, selected - 1)
+                    elif event.key == pygame.K_DOWN:
+                        selected = min(len(options) - 1, selected + 1)
+                    elif event.key == pygame.K_RETURN:
+                        if selected == 0:
+                            return self.perform_update()
+                        return False
+                    elif event.key == pygame.K_ESCAPE:
+                        return False
+                if event.type == pygame.JOYBUTTONDOWN:
+                    if event.button == 0:
+                        if selected == 0:
+                            return self.perform_update()
+                        return False
+                    elif event.button == 1:
+                        return False
+
+            if self.joystick and self.joystick.get_numhats() > 0:
+                hat = self.joystick.get_hat(0)
+                if hat[1] == 1:
+                    selected = max(0, selected - 1)
+                    pygame.time.wait(150)
+                elif hat[1] == -1:
+                    selected = min(len(options) - 1, selected + 1)
+                    pygame.time.wait(150)
+
+            self.clock.tick(30)
+
+    def perform_update(self):
+        """Perform the git update and show result"""
+        self.draw_message("Updating...", "Pulling latest changes from git")
+        pygame.display.flip()
+
+        success, message = self.update_checker.update()
+
+        if success:
+            self.draw_message("Update Complete!", "Please restart the tool")
+            pygame.display.flip()
+            pygame.time.wait(3000)
+            pygame.quit()
+            sys.exit(0)
+        else:
+            self.draw_message("Update Failed", message[:50])
+            pygame.display.flip()
+            pygame.time.wait(3000)
+            return False
+
+
     def download_all(self):
         missing_games = [(name, href) for name, href, status in self.filtered_games if not status]
         if not missing_games:
 def download_all(self):
     def run(self):
         os.makedirs(ROM_DIR, exist_ok=True)
+
+        # Check for updates at startup
+        self.check_for_updates()
         self.existing_games = self.get_existing_games()
         self.games = self.fetch_game_list()
Read more...