                        # Currently unset, go to first choice
                        opt.current_value = opt.choices[0] if opt.choices else ""
                    elif opt.current_value not in opt.choices:
                        # Invalid value, go to first choice
                        opt.current_value = opt.choices[0] if opt.choices else ""
                    else:
                        # Valid value, cycle to next (or back to None after last)
                        idx = opt.choices.index(opt.current_value)
                        if idx == len(opt.choices) - 1:
                            # Last choice, cycle back to unset (None)
                            opt.current_value = None
                        else:
                            # Go to next choice
                            opt.current_value = opt.choices[idx + 1]
                    self.modified = True

                case "int" | "string":
                    # For string/int, Enter opens the editor
                    self.handle_edit()

    def handle_edit(self) -> None:
        """Handle editing current item value."""
        if self.current_row >= len(self.display_items):
            return

        item = self.display_items[self.current_row]

        if item["type"] == "option":
            opt = item["option"]

            if opt.var_type in ["int", "string", "multi"]:
                height, width = self.stdscr.getmaxyx()

                # Calculate the Y position of the current row on screen
                # Rows start at y=2 (after header), and we need to account for scroll
                screen_y = 2 + (self.current_row - self.top_row)

                # Make sure we're within visible area
                if screen_y < 2 or screen_y >= height - 5:
                    # Fall back to center if somehow out of range
                    screen_y = height // 2

                # Build the display name with [multi] tag if applicable
                display_name = opt.get_display_name()
                if opt.var_type == "multi":
                    display_name += " [multi]"

                # Get current value to pre-fill
                current_val = opt.current_value if opt.current_value else ""

                # Create prompt: "name = |cursor here|"
                prefix = "    "
                prompt_text = f"{prefix}{display_name} = "

                # Clear the line and show the prompt
                self.stdscr.move(screen_y, 0)
                self.stdscr.clrtoeol()
                self.stdscr.addstr(screen_y, 0, prompt_text, curses.A_BOLD)

                # Pre-fill with current value
                if current_val:
                    self.stdscr.addstr(screen_y, len(prompt_text), current_val)

                self.stdscr.refresh()

                # Position cursor at the end of the current value
                cursor_pos = len(prompt_text) + len(current_val)
                self.stdscr.move(screen_y, cursor_pos)

                # Enable cursor and echo
                curses.curs_set(1)
                curses.echo()

                try:
                    # Manual text editing with basic line editing support
                    buffer = list(current_val)  # Start with current value
                    cursor_offset = len(buffer)  # Cursor at end
                    max_len = width - len(prompt_text) - 2

                    while True:
                        # Display current buffer
                        self.stdscr.move(screen_y, len(prompt_text))
                        self.stdscr.clrtoeol()
                        display_text = "".join(buffer)
                        if len(display_text) > max_len:
                            display_text = display_text[:max_len]
                        self.stdscr.addstr(screen_y, len(prompt_text), display_text)
                        self.stdscr.move(screen_y, len(prompt_text) + cursor_offset)
                        self.stdscr.refresh()

                        # Get key
                        key = self.stdscr.getch()

                        if key == ord("\n") or key == curses.KEY_ENTER:
                            # Accept input
                            value = "".join(buffer).strip()
                            if value or opt.current_value:  # Allow clearing
                                opt.current_value = value if value else None
                                self.modified = True
                            break
                        elif key == 27:  # Escape
                            # Cancel editing
                            break
                        elif key == curses.KEY_BACKSPACE or key == 127 or key == 8:
                            # Backspace
                            if cursor_offset > 0:
                                buffer.pop(cursor_offset - 1)
                                cursor_offset -= 1
                        elif key == curses.KEY_DC:
                            # Delete key
                            if cursor_offset < len(buffer):
                                buffer.pop(cursor_offset)
                        elif key == curses.KEY_LEFT:
                            # Move cursor left
                            if cursor_offset > 0:
                                cursor_offset -= 1
                        elif key == curses.KEY_RIGHT:
                            # Move cursor right
                            if cursor_offset < len(buffer):
                                cursor_offset += 1
                        elif key == curses.KEY_HOME or key == 1:  # Ctrl-A
                            # Move to start
                            cursor_offset = 0
                        elif key == curses.KEY_END or key == 5:  # Ctrl-E
                            # Move to end
                            cursor_offset = len(buffer)
                        elif key == 21:  # Ctrl-U
                            # Clear line
                            buffer = []
                            cursor_offset = 0
                        elif 32 <= key <= 126:
                            # Printable character
                            if len(buffer) < max_len:
                                buffer.insert(cursor_offset, chr(key))
                                cursor_offset += 1

                except Exception:
                    pass
                finally:
                    curses.noecho()
                    curses.curs_set(0)

    def show_confirmation(self, message: str, prompt: str) -> str:
        """Show a simple y/n confirmation dialog.

        Returns the character pressed by the user (y/n/etc).
        """
        self.stdscr.clear()
        height, width = self.stdscr.getmaxyx()

        # Center the message
        y = height // 2
        x = max(0, (width - len(message)) // 2)
        self.stdscr.addstr(y, x, message, curses.A_BOLD)

        # Show prompt below
        y += 2
        x = max(0, (width - len(prompt)) // 2)
        self.stdscr.addstr(y, x, prompt)

        self.stdscr.refresh()


                            cursor_offset = 0
                        elif key == curses.KEY_END or key == 5:  # Ctrl-E
                            # Move to end
                            cursor_offset = len(buffer)
                        elif key == 21:  # Ctrl-U
                            # Clear line
                            buffer = []
                            cursor_offset = 0
                        elif 32 <= key <= 126:
                            # Printable character
                            if len(buffer) < max_len:
                                buffer.insert(cursor_offset, chr(key))
                                cursor_offset += 1

                except Exception:
                    pass
                finally:
                    curses.noecho()
                    curses.curs_set(0)

    def show_confirmation(self, message: str, prompt: str) -> str:
        """Show a simple y/n confirmation dialog.

        Returns the character pressed by the user (y/n/etc).
        """
        self.stdscr.clear()
        height, width = self.stdscr.getmaxyx()

        # Center the message
        y = height // 2
        x = max(0, (width - len(message)) // 2)
        self.stdscr.addstr(y, x, message, curses.A_BOLD)

        # Show prompt below
        y += 2
        x = max(0, (width - len(prompt)) // 2)
        self.stdscr.addstr(y, x, prompt)

        self.stdscr.refresh()

        # Get single character response
        response = self.stdscr.getch()
        if response == 27:  # Escape
            return "n"
        return chr(response) if response < 256 else "n"

    def show_message(
        self, message: str, submessage: str = "", wait_for_key: bool = True
    ) -> None:
        """Show a simple message dialog"""
        self.stdscr.clear()
        height, width = self.stdscr.getmaxyx()

        # Center the message
        y = height // 2
        x = max(0, (width - len(message)) // 2)
        self.stdscr.addstr(y, x, message, curses.A_BOLD)

        # Show submessage below
        if submessage:
            y += 2
            x = max(0, (width - len(submessage)) // 2)
            self.stdscr.addstr(y, x, submessage)

        if wait_for_key:


        height, width = self.stdscr.getmaxyx()

        # Center the message
        y = height // 2
        x = max(0, (width - len(message)) // 2)
        self.stdscr.addstr(y, x, message, curses.A_BOLD)

        # Show prompt below
        y += 2
        x = max(0, (width - len(prompt)) // 2)
        self.stdscr.addstr(y, x, prompt)

        self.stdscr.refresh()

        # Get single character response
        response = self.stdscr.getch()
        if response == 27:  # Escape
            return "n"
        return chr(response) if response < 256 else "n"

    def show_message(
        self, message: str, submessage: str = "", wait_for_key: bool = True
    ) -> None:
        """Show a simple message dialog"""
        self.stdscr.clear()
        height, width = self.stdscr.getmaxyx()

        # Center the message
        y = height // 2
        x = max(0, (width - len(message)) // 2)
        self.stdscr.addstr(y, x, message, curses.A_BOLD)

        # Show submessage below
        if submessage:
            y += 2
            x = max(0, (width - len(submessage)) // 2)
            self.stdscr.addstr(y, x, submessage)

        if wait_for_key:
            y += 2
            prompt = "Press any key to continue..."
            x = max(0, (width - len(prompt)) // 2)
            self.stdscr.addstr(y, x, prompt)

        self.stdscr.refresh()

        if wait_for_key:
            self.stdscr.getch()

    def handle_apply_now(self) -> None:
        """Show confirmation, generate apply script, and signal to exit"""
        # Import save_config here to avoid circular import
        from configure_env import save_config
        import os
        import sys
        import tempfile

        # Show confirmation dialog
        response = self.show_confirmation("Apply configuration and exit?", "[y/N]")

        if response not in ["y", "Y"]:
            return  # User cancelled

        # Get exports and unsets
        exports = self.config.get_env_exports()
        unsets = self.config.get_unset_vars()

        # Generate apply script with unpredictable name in current directory


        x = max(0, (width - len(message)) // 2)
        self.stdscr.addstr(y, x, message, curses.A_BOLD)

        # Show submessage below
        if submessage:
            y += 2
            x = max(0, (width - len(submessage)) // 2)
            self.stdscr.addstr(y, x, submessage)

        if wait_for_key:
            y += 2
            prompt = "Press any key to continue..."
            x = max(0, (width - len(prompt)) // 2)
            self.stdscr.addstr(y, x, prompt)

        self.stdscr.refresh()

        if wait_for_key:
            self.stdscr.getch()

    def handle_apply_now(self) -> None:
        """Show confirmation, generate apply script, and signal to exit"""
        # Import save_config here to avoid circular import
        from configure_env import save_config
        import os
        import sys
        import tempfile

        # Show confirmation dialog
        response = self.show_confirmation("Apply configuration and exit?", "[y/N]")

        if response not in ["y", "Y"]:
            return  # User cancelled

        # Get exports and unsets
        exports = self.config.get_env_exports()
        unsets = self.config.get_unset_vars()

        # Generate apply script with unpredictable name in current directory
        # Using mktemp pattern for security (prevents race conditions)
        fd, apply_script = tempfile.mkstemp(
            suffix=".sh", prefix=".nvfuser-apply.", dir=os.getcwd(), text=True
        )
        os.close(fd)  # Close the file descriptor, we'll write via save_config

        # Debug output
        if os.environ.get("NVFUSER_CONFIG_DEBUG") == "1":
            sys.stderr.write(f"[DEBUG] Creating apply script at: {apply_script}\n")
            sys.stderr.write(f"[DEBUG] Exports: {len(exports)} variables\n")
            sys.stderr.write(f"[DEBUG] Unsets: {len(unsets)} variables\n")

        save_config(exports, unsets, apply_script)

        if os.environ.get("NVFUSER_CONFIG_DEBUG") == "1":
            if os.path.exists(apply_script):
                sys.stderr.write("[DEBUG] Apply script created successfully\n")
            else:
                sys.stderr.write("[DEBUG] ERROR: Apply script was NOT created!\n")

        # Set flag to exit
        self.should_exit = True
        self.modified = False  # Prevent quit prompt

    def handle_generate(self) -> None:
        """Prompt for filename and generate script"""
        # Import save_config here to avoid circular import
        from configure_env import save_config

        # Simple text input prompt
        self.stdscr.clear()
        height, width = self.stdscr.getmaxyx()

        # Show prompt
        prompt = "Enter filename [nvfuser_env.sh]: "
        self.stdscr.addstr(height // 2, 2, prompt, curses.A_BOLD)
        self.stdscr.refresh()

        # Enable echo for input
        curses.echo()
        curses.curs_set(1)

        # Get input (basic - no arrow key editing, but backspace works naturally)


        os.close(fd)  # Close the file descriptor, we'll write via save_config

        # Debug output
        if os.environ.get("NVFUSER_CONFIG_DEBUG") == "1":
            sys.stderr.write(f"[DEBUG] Creating apply script at: {apply_script}\n")
            sys.stderr.write(f"[DEBUG] Exports: {len(exports)} variables\n")
            sys.stderr.write(f"[DEBUG] Unsets: {len(unsets)} variables\n")

        save_config(exports, unsets, apply_script)

        if os.environ.get("NVFUSER_CONFIG_DEBUG") == "1":
            if os.path.exists(apply_script):
                sys.stderr.write("[DEBUG] Apply script created successfully\n")
            else:
                sys.stderr.write("[DEBUG] ERROR: Apply script was NOT created!\n")

        # Set flag to exit
        self.should_exit = True
        self.modified = False  # Prevent quit prompt

    def handle_generate(self) -> None:
        """Prompt for filename and generate script"""
        # Import save_config here to avoid circular import
        from configure_env import save_config

        # Simple text input prompt
        self.stdscr.clear()
        height, width = self.stdscr.getmaxyx()

        # Show prompt
        prompt = "Enter filename [nvfuser_env.sh]: "
        self.stdscr.addstr(height // 2, 2, prompt, curses.A_BOLD)
        self.stdscr.refresh()

        # Enable echo for input
        curses.echo()
        curses.curs_set(1)

        # Get input (basic - no arrow key editing, but backspace works naturally)
        try:
            filename_input = (
                self.stdscr.getstr(height // 2, 2 + len(prompt), 50)
                .decode("utf-8")
                .strip()
            )
        finally:
            curses.noecho()
            curses.curs_set(0)

        # Use default if empty
        if not filename_input:
            filename = "nvfuser_env.sh"
        else:
            filename = filename_input

        # Get exports and unsets
        exports = self.config.get_env_exports()
        unsets = self.config.get_unset_vars()

        # Generate script
        save_config(exports, unsets, filename)

        # Show confirmation dialog
        self.show_message(
            f"Configuration saved to {filename}",
            f"To apply: source {filename}",
            wait_for_key=True,
        )

        # Exit
        self.should_exit = True
        self.modified = False

    def reload_from_environment(self) -> None:
        """Reload configuration values from current environment."""
        self.config._load_current_values()
        self.modified = False

    def jump_to_next_section(self) -> None:
        """Jump to the next section header."""
        for i in range(self.current_row + 1, len(self.display_items)):
            if self.display_items[i]["type"] == "header":
                self.current_row = i
                # Adjust scroll if needed
                height, _ = self.stdscr.getmaxyx()
                visible_rows = height - 7
                if self.current_row >= self.top_row + visible_rows:
                    self.top_row = self.current_row - visible_rows + 1
                break

    def jump_to_prev_section(self) -> None:
        """Jump to the previous section header."""


            filename = filename_input

        # Get exports and unsets
        exports = self.config.get_env_exports()
        unsets = self.config.get_unset_vars()

        # Generate script
        save_config(exports, unsets, filename)

        # Show confirmation dialog
        self.show_message(
            f"Configuration saved to {filename}",
            f"To apply: source {filename}",
            wait_for_key=True,
        )

        # Exit
        self.should_exit = True
        self.modified = False

    def reload_from_environment(self) -> None:
        """Reload configuration values from current environment."""
        self.config._load_current_values()
        self.modified = False

    def jump_to_next_section(self) -> None:
        """Jump to the next section header."""
        for i in range(self.current_row + 1, len(self.display_items)):
            if self.display_items[i]["type"] == "header":
                self.current_row = i
                # Adjust scroll if needed
                height, _ = self.stdscr.getmaxyx()
                visible_rows = height - 7
                if self.current_row >= self.top_row + visible_rows:
                    self.top_row = self.current_row - visible_rows + 1
                break

    def jump_to_prev_section(self) -> None:
        """Jump to the previous section header."""
        for i in range(self.current_row - 1, -1, -1):
            if self.display_items[i]["type"] == "header":
                self.current_row = i
                # Adjust scroll if needed
                if self.current_row < self.top_row:


        # Generate script
        save_config(exports, unsets, filename)

        # Show confirmation dialog
        self.show_message(
            f"Configuration saved to {filename}",
            f"To apply: source {filename}",
            wait_for_key=True,
        )

        # Exit
        self.should_exit = True
        self.modified = False

    def reload_from_environment(self) -> None:
        """Reload configuration values from current environment."""
        self.config._load_current_values()
        self.modified = False

    def jump_to_next_section(self) -> None:
        """Jump to the next section header."""
        for i in range(self.current_row + 1, len(self.display_items)):
            if self.display_items[i]["type"] == "header":
                self.current_row = i
                # Adjust scroll if needed
                height, _ = self.stdscr.getmaxyx()
                visible_rows = height - 7
                if self.current_row >= self.top_row + visible_rows:
                    self.top_row = self.current_row - visible_rows + 1
                break

    def jump_to_prev_section(self) -> None:
        """Jump to the previous section header."""
        for i in range(self.current_row - 1, -1, -1):
            if self.display_items[i]["type"] == "header":
                self.current_row = i
                # Adjust scroll if needed
                if self.current_row < self.top_row:
                    self.top_row = self.current_row
                break

    def jump_to_top(self) -> None:
        """Jump to the first item."""
        self.current_row = 0
        self.top_row = 0

    def jump_to_bottom(self) -> None:
        """Jump to the last item."""
        self.current_row = len(self.display_items) - 1
        # Adjust scroll to show bottom


        self.should_exit = True
        self.modified = False

    def reload_from_environment(self) -> None:
        """Reload configuration values from current environment."""
        self.config._load_current_values()
        self.modified = False

    def jump_to_next_section(self) -> None:
        """Jump to the next section header."""
        for i in range(self.current_row + 1, len(self.display_items)):
            if self.display_items[i]["type"] == "header":
                self.current_row = i
                # Adjust scroll if needed
                height, _ = self.stdscr.getmaxyx()
                visible_rows = height - 7
                if self.current_row >= self.top_row + visible_rows:
                    self.top_row = self.current_row - visible_rows + 1
                break

    def jump_to_prev_section(self) -> None:
        """Jump to the previous section header."""
        for i in range(self.current_row - 1, -1, -1):
            if self.display_items[i]["type"] == "header":
                self.current_row = i
                # Adjust scroll if needed
                if self.current_row < self.top_row:
                    self.top_row = self.current_row
                break

    def jump_to_top(self) -> None:
        """Jump to the first item."""
        self.current_row = 0
        self.top_row = 0

    def jump_to_bottom(self) -> None:
        """Jump to the last item."""
        self.current_row = len(self.display_items) - 1
        # Adjust scroll to show bottom
        height, _ = self.stdscr.getmaxyx()
        visible_rows = height - 7
        self.top_row = max(0, self.current_row - visible_rows + 1)

    def search_items(self, query: str) -> list[int]:
        """Search for items matching the query.

        Returns list of indices of matching items (both headers and options).
        Searches in option names and category headers only.
        Case-insensitive search.


        for i in range(self.current_row + 1, len(self.display_items)):
            if self.display_items[i]["type"] == "header":
                self.current_row = i
                # Adjust scroll if needed
                height, _ = self.stdscr.getmaxyx()
                visible_rows = height - 7
                if self.current_row >= self.top_row + visible_rows:
                    self.top_row = self.current_row - visible_rows + 1
                break

    def jump_to_prev_section(self) -> None:
        """Jump to the previous section header."""
        for i in range(self.current_row - 1, -1, -1):
            if self.display_items[i]["type"] == "header":
                self.current_row = i
                # Adjust scroll if needed
                if self.current_row < self.top_row:
                    self.top_row = self.current_row
                break

    def jump_to_top(self) -> None:
        """Jump to the first item."""
        self.current_row = 0
        self.top_row = 0

    def jump_to_bottom(self) -> None:
        """Jump to the last item."""
        self.current_row = len(self.display_items) - 1
        # Adjust scroll to show bottom
        height, _ = self.stdscr.getmaxyx()
        visible_rows = height - 7
        self.top_row = max(0, self.current_row - visible_rows + 1)

    def search_items(self, query: str) -> list[int]:
        """Search for items matching the query.

        Returns list of indices of matching items (both headers and options).
        Searches in option names and category headers only.
        Case-insensitive search.
        """
        if not query:
            return []

        matches = []


                visible_rows = height - 7
                if self.current_row >= self.top_row + visible_rows:
                    self.top_row = self.current_row - visible_rows + 1
                break

    def jump_to_prev_section(self) -> None:
        """Jump to the previous section header."""
        for i in range(self.current_row - 1, -1, -1):
            if self.display_items[i]["type"] == "header":
                self.current_row = i
                # Adjust scroll if needed
                if self.current_row < self.top_row:
                    self.top_row = self.current_row
                break

    def jump_to_top(self) -> None:
        """Jump to the first item."""
        self.current_row = 0
        self.top_row = 0

    def jump_to_bottom(self) -> None:
        """Jump to the last item."""
        self.current_row = len(self.display_items) - 1
        # Adjust scroll to show bottom
        height, _ = self.stdscr.getmaxyx()
        visible_rows = height - 7
        self.top_row = max(0, self.current_row - visible_rows + 1)

    def search_items(self, query: str) -> list[int]:
        """Search for items matching the query.

        Returns list of indices of matching items (both headers and options).
        Searches in option names and category headers only.
        Case-insensitive search.
        """
        if not query:
            return []

        matches = []
        query_lower = query.lower()

        for i, item in enumerate(self.display_items):
            if item["type"] == "header":
                # Search in category header text
                if query_lower in item["text"].lower():
                    matches.append(i)
            elif item["type"] == "option":


            if self.display_items[i]["type"] == "header":
                self.current_row = i
                # Adjust scroll if needed
                if self.current_row < self.top_row:
                    self.top_row = self.current_row
                break

    def jump_to_top(self) -> None:
        """Jump to the first item."""
        self.current_row = 0
        self.top_row = 0

    def jump_to_bottom(self) -> None:
        """Jump to the last item."""
        self.current_row = len(self.display_items) - 1
        # Adjust scroll to show bottom
        height, _ = self.stdscr.getmaxyx()
        visible_rows = height - 7
        self.top_row = max(0, self.current_row - visible_rows + 1)

    def search_items(self, query: str) -> list[int]:
        """Search for items matching the query.

        Returns list of indices of matching items (both headers and options).
        Searches in option names and category headers only.
        Case-insensitive search.
        """
        if not query:
            return []

        matches = []
        query_lower = query.lower()

        for i, item in enumerate(self.display_items):
            if item["type"] == "header":
                # Search in category header text
                if query_lower in item["text"].lower():
                    matches.append(i)
            elif item["type"] == "option":
                opt = item["option"]
                # Search in option name only
                if query_lower in opt.name.lower():
                    matches.append(i)

        return matches

    def handle_search(self) -> None:
        """Enter search mode and handle search input."""
        height, width = self.stdscr.getmaxyx()

        # Enable cursor for search input
        curses.curs_set(1)
        curses.echo()

        search_query = ""

        try:
            while True:
                # Draw the search prompt
                self.stdscr.move(height - 1, 0)
                self.stdscr.clrtoeol()
                self.stdscr.attron(curses.color_pair(2))
                prompt = f"/{search_query}"
                self.stdscr.addstr(height - 1, 0, prompt.ljust(width - 1))
                self.stdscr.attroff(curses.color_pair(2))


        """
        if not query:
            return []

        matches = []
        query_lower = query.lower()

        for i, item in enumerate(self.display_items):
            if item["type"] == "header":
                # Search in category header text
                if query_lower in item["text"].lower():
                    matches.append(i)
            elif item["type"] == "option":
                opt = item["option"]
                # Search in option name only
                if query_lower in opt.name.lower():
                    matches.append(i)

        return matches

    def handle_search(self) -> None:
        """Enter search mode and handle search input."""
        height, width = self.stdscr.getmaxyx()

        # Enable cursor for search input
        curses.curs_set(1)
        curses.echo()

        search_query = ""

        try:
            while True:
                # Draw the search prompt
                self.stdscr.move(height - 1, 0)
                self.stdscr.clrtoeol()
                self.stdscr.attron(curses.color_pair(2))
                prompt = f"/{search_query}"
                self.stdscr.addstr(height - 1, 0, prompt.ljust(width - 1))
                self.stdscr.attroff(curses.color_pair(2))
                self.stdscr.move(height - 1, len(prompt))
                self.stdscr.refresh()

                # Get key
                key = self.stdscr.getch()

                if key == ord("\n") or key == curses.KEY_ENTER:
                    # Accept search and jump to first match
                    if search_query:
                        self.search_query = search_query
                        self.search_matches = self.search_items(search_query)
                        if self.search_matches:
                            self.search_match_index = 0
                            self.jump_to_search_match(self.search_matches[0])
                    break
                elif key == 27:  # Escape
                    # Cancel search
                    break
                elif key == curses.KEY_BACKSPACE or key == 127 or key == 8:
                    # Backspace
                    if search_query:
                        search_query = search_query[:-1]
                elif 32 <= key <= 126:
                    # Printable character
                    search_query += chr(key)
        finally:
            curses.noecho()
            curses.curs_set(0)

    def jump_to_search_match(self, match_index: int) -> None:
        """Jump to a specific item by index."""
        if 0 <= match_index < len(self.display_items):
            self.current_row = match_index
            # Adjust scroll to center the match if possible
            height, _ = self.stdscr.getmaxyx()
            visible_rows = height - 7
            # Try to center the match
            target_top = max(0, self.current_row - visible_rows // 2)
            # But make sure we don't scroll past the end
            max_top = max(0, len(self.display_items) - visible_rows)
            self.top_row = min(target_top, max_top)

    def jump_to_next_match(self) -> None:
        """Jump to next search match (n in vim)."""
        if not self.search_matches:
            return

        self.search_match_index = (self.search_match_index + 1) % len(


                        self.search_query = search_query
                        self.search_matches = self.search_items(search_query)
                        if self.search_matches:
                            self.search_match_index = 0
                            self.jump_to_search_match(self.search_matches[0])
                    break
                elif key == 27:  # Escape
                    # Cancel search
                    break
                elif key == curses.KEY_BACKSPACE or key == 127 or key == 8:
                    # Backspace
                    if search_query:
                        search_query = search_query[:-1]
                elif 32 <= key <= 126:
                    # Printable character
                    search_query += chr(key)
        finally:
            curses.noecho()
            curses.curs_set(0)

    def jump_to_search_match(self, match_index: int) -> None:
        """Jump to a specific item by index."""
        if 0 <= match_index < len(self.display_items):
            self.current_row = match_index
            # Adjust scroll to center the match if possible
            height, _ = self.stdscr.getmaxyx()
            visible_rows = height - 7
            # Try to center the match
            target_top = max(0, self.current_row - visible_rows // 2)
            # But make sure we don't scroll past the end
            max_top = max(0, len(self.display_items) - visible_rows)
            self.top_row = min(target_top, max_top)

    def jump_to_next_match(self) -> None:
        """Jump to next search match (n in vim)."""
        if not self.search_matches:
            return

        self.search_match_index = (self.search_match_index + 1) % len(
            self.search_matches
        )
        self.jump_to_search_match(self.search_matches[self.search_match_index])

    def jump_to_prev_match(self) -> None:
        """Jump to previous search match (N in vim)."""
        if not self.search_matches:
            return

        self.search_match_index = (self.search_match_index - 1) % len(
            self.search_matches
        )
        self.jump_to_search_match(self.search_matches[self.search_match_index])


                elif 32 <= key <= 126:
                    # Printable character
                    search_query += chr(key)
        finally:
            curses.noecho()
            curses.curs_set(0)

    def jump_to_search_match(self, match_index: int) -> None:
        """Jump to a specific item by index."""
        if 0 <= match_index < len(self.display_items):
            self.current_row = match_index
            # Adjust scroll to center the match if possible
            height, _ = self.stdscr.getmaxyx()
            visible_rows = height - 7
            # Try to center the match
            target_top = max(0, self.current_row - visible_rows // 2)
            # But make sure we don't scroll past the end
            max_top = max(0, len(self.display_items) - visible_rows)
            self.top_row = min(target_top, max_top)

    def jump_to_next_match(self) -> None:
        """Jump to next search match (n in vim)."""
        if not self.search_matches:
            return

        self.search_match_index = (self.search_match_index + 1) % len(
            self.search_matches
        )
        self.jump_to_search_match(self.search_matches[self.search_match_index])

    def jump_to_prev_match(self) -> None:
        """Jump to previous search match (N in vim)."""
        if not self.search_matches:
            return

        self.search_match_index = (self.search_match_index - 1) % len(
            self.search_matches
        )
        self.jump_to_search_match(self.search_matches[self.search_match_index])

    def run(self) -> None:
        """Main event loop."""
        while True:
            self.draw()

            key = self.stdscr.getch()

            if key == ord("q") or key == ord("Q"):
                # Quit


            self.current_row = match_index
            # Adjust scroll to center the match if possible
            height, _ = self.stdscr.getmaxyx()
            visible_rows = height - 7
            # Try to center the match
            target_top = max(0, self.current_row - visible_rows // 2)
            # But make sure we don't scroll past the end
            max_top = max(0, len(self.display_items) - visible_rows)
            self.top_row = min(target_top, max_top)

    def jump_to_next_match(self) -> None:
        """Jump to next search match (n in vim)."""
        if not self.search_matches:
            return

        self.search_match_index = (self.search_match_index + 1) % len(
            self.search_matches
        )
        self.jump_to_search_match(self.search_matches[self.search_match_index])

    def jump_to_prev_match(self) -> None:
        """Jump to previous search match (N in vim)."""
        if not self.search_matches:
            return

        self.search_match_index = (self.search_match_index - 1) % len(
            self.search_matches
        )
        self.jump_to_search_match(self.search_matches[self.search_match_index])

    def run(self) -> None:
        """Main event loop."""
        while True:
            self.draw()

            key = self.stdscr.getch()

            if key == ord("q") or key == ord("Q"):
                # Quit
                if self.modified:
                    # Ask what to do with changes
                    height, width = self.stdscr.getmaxyx()
                    msg_y = height // 2 - 3

                    self.stdscr.clear()
                    self.stdscr.attron(curses.color_pair(1))

                    title = "You have unsaved changes!"
                    self.stdscr.addstr(msg_y, (width - len(title)) // 2, title)


run

    def jump_to_next_match(self) -> None:
        """Jump to next search match (n in vim)."""
        if not self.search_matches:
            return

        self.search_match_index = (self.search_match_index + 1) % len(
            self.search_matches
        )
        self.jump_to_search_match(self.search_matches[self.search_match_index])

    def jump_to_prev_match(self) -> None:
        """Jump to previous search match (N in vim)."""
        if not self.search_matches:
            return

        self.search_match_index = (self.search_match_index - 1) % len(
            self.search_matches
        )
        self.jump_to_search_match(self.search_matches[self.search_match_index])

    def run(self) -> None:
        """Main event loop."""
        while True:
            self.draw()

            key = self.stdscr.getch()

            if key == ord("q") or key == ord("Q"):
                # Quit
                if self.modified:
                    # Ask what to do with changes
                    height, width = self.stdscr.getmaxyx()
                    msg_y = height // 2 - 3

                    self.stdscr.clear()
                    self.stdscr.attron(curses.color_pair(1))

                    title = "You have unsaved changes!"
                    self.stdscr.addstr(msg_y, (width - len(title)) // 2, title)

                    self.stdscr.attroff(curses.color_pair(1))

                    # Show options
                    options = [
                        "",
                        "What would you like to do?",
                        "",
                        "  [a] Apply Now    - Generate .nvfuser_apply.*.sh in current dir",
                        "                     (sourced automatically after exit)",
                        "",
                        "  [g] Generate     - Create nvfuser_env.sh",
                        "                     (then source it after exit)",
                        "",
                        "  [q] Quit         - Exit without saving",
                        "",
                        "  [Esc] Cancel     - Return to configuration",
                    ]

                    for i, line in enumerate(options):
                        if line.startswith("  ["):
                            # Highlight the option keys
                            self.stdscr.attron(curses.A_BOLD)
                        self.stdscr.addstr(
                            msg_y + 2 + i, (width - len(line)) // 2, line
                        )
                        if line.startswith("  ["):
                            self.stdscr.attroff(curses.A_BOLD)

                    self.stdscr.refresh()

                    # Get response
                    response = self.stdscr.getch()

                    if response == ord("a") or response == ord("A"):
                        self.handle_apply_now()
                        break
                    elif response == ord("g") or response == ord("G"):
                        self.handle_generate()
                        break
                    elif response == ord("q") or response == ord("Q"):
                        break
                    elif response == 27:  # Escape
                        continue  # Return to main loop
                    else:
                        # Invalid key, show again
                        continue
                else:
                    # No changes, just quit
                    break

            elif key == curses.KEY_UP:
                if self.current_row > 0:
                    self.current_row -= 1
                    # Adjust scroll if needed
                    if self.current_row < self.top_row:
                        self.top_row = self.current_row

            elif key == curses.KEY_DOWN:
                if self.current_row < len(self.display_items) - 1:
                    self.current_row += 1
                    # Adjust scroll if needed
                    height, width = self.stdscr.getmaxyx()
                    visible_rows = height - 7
                    if self.current_row >= self.top_row + visible_rows:
                        self.top_row = self.current_row - visible_rows + 1

            # Vim-style navigation
            elif key == ord("k") or key == ord("K"):
                # Up (same as arrow up)
                if self.current_row > 0:
                    self.current_row -= 1
                    if self.current_row < self.top_row:
                        self.top_row = self.current_row

            elif key == ord("j") or key == ord("J"):
                # Down (same as arrow down)
                if self.current_row < len(self.display_items) - 1:
                    self.current_row += 1
                    height, width = self.stdscr.getmaxyx()
                    visible_rows = height - 7
                    if self.current_row >= self.top_row + visible_rows:
                        self.top_row = self.current_row - visible_rows + 1

            elif key == ord("}"):
                # Jump to next section
                self.jump_to_next_section()

            elif key == ord("{"):
                # Jump to previous section
                self.jump_to_prev_section()

            elif key == curses.KEY_PPAGE:
                # Page Up - jump to top
                self.jump_to_top()

            elif key == curses.KEY_NPAGE:
                # Page Down - jump to bottom
                self.jump_to_bottom()

            elif key == ord(" ") or key == ord("\n") or key == curses.KEY_ENTER:
                self.handle_toggle()

            elif key == ord("e") or key == ord("E"):
                self.handle_edit()

            elif key == ord("r") or key == ord("R"):
                # Reload from environment
                self.reload_from_environment()

            elif key == ord("a") or key == ord("A"):
                self.handle_apply_now()
                if self.should_exit:
                    break  # Exit immediately after apply

            elif key == ord("g") or key == ord("G"):
                self.handle_generate()
                if self.should_exit:
                    break  # Exit immediately after generate

            elif key == ord("/"):
                # Enter search mode
                self.handle_search()

            elif key == ord("n"):
                # Jump to next search match
                self.jump_to_next_match()

            elif key == ord("N"):
                # Jump to previous search match
                self.jump_to_prev_match()

def run_curses_ui(stdscr, config: EnvVarConfig) -> None:
    """Entry point for curses UI."""
    ui = CursesUI(stdscr, config)
    ui.run()


#!/usr/bin/env python3
# SPDX-FileCopyrightText: Copyright (c) 2025-present NVIDIA CORPORATION & AFFILIATES.
# All rights reserved.
# SPDX-License-Identifier: BSD-3-Clause

"""
Interactive nvFuser Environment Configuration Tool

This tool provides an interactive interface (similar to ccmake) for configuring
nvFuser environment variables. It helps users set up debug flags, feature toggles,
and runtime options without needing to remember complex NVFUSER_* variable names.

Usage:
    python tools/configure_env.py                    # Interactive TUI mode
    python tools/configure_env.py --simple           # Simple prompt mode
    python tools/configure_env.py --generate-script  # Generate shell script
"""

from __future__ import annotations

import argparse
import os
from pathlib import Path

# Load environment options from YAML
from options_loader import load_options_from_yaml, EnvVarOption

# Load configuration from YAML file
_yaml_path = Path(__file__).parent / "env_options.yaml"
CATEGORY_NAMES, ENV_VAR_DEFINITIONS = load_options_from_yaml(_yaml_path)

class EnvVarConfig:
    """Manages the current configuration state."""

    def __init__(self) -> None:
        # Use (name, env_var) tuple as key to handle options with same name
        # but different env vars (e.g., expr_simplify in DUMP vs DISABLE)
        self.options: dict[tuple[str, str | None], EnvVarOption] = {
            (opt.name, opt.env_var): opt for opt in ENV_VAR_DEFINITIONS
        }
        # Also maintain a list of all options for iteration
        self.all_options: list[EnvVarOption] = ENV_VAR_DEFINITIONS.copy()
        self.categories: dict[str, list[EnvVarOption]] = self._organize_by_category()
        self._load_current_values()

    def _organize_by_category(self) -> dict[str, list[EnvVarOption]]:
        """Organize options by category."""
        categories: dict[str, list[EnvVarOption]] = {}
        for opt in self.all_options:
            if opt.category not in categories:
                categories[opt.category] = []
            categories[opt.category].append(opt)
        return categories

    def _load_current_values(self) -> None:
        """Load current values from environment."""
        for opt in self.all_options:
            env_var_name = opt.get_env_var_name()

            if opt.category in ["dump", "enable", "disable"]:
                # These are comma-separated list values
                list_val = os.environ.get(env_var_name, "")
                if list_val:
                    list_items = [item.strip() for item in list_val.split(",")]
                    if opt.name in list_items:
                        opt.current_value = "1"
            else:
                # Regular environment variables
                if env_var_name in os.environ:
                    val = os.environ[env_var_name]
                    if opt.var_type == "bool":
                        if val.upper() in ["ON", "1", "YES", "TRUE", "Y"]:
                            opt.current_value = "1"
                    else:
                        opt.current_value = val

    def get_env_exports(self) -> dict[str, str]:
        """Generate environment variable exports based on current configuration."""
        exports: dict[str, str] = {}

        # Collect values for list-based env vars (DUMP, ENABLE, DISABLE)
        list_vars: dict[str, list[str]] = {}  # Maps env_var_name -> list of values

        for opt in self.all_options:
            if opt.category in ["dump", "enable", "disable"]:
                # List items
                if opt.current_value == "1":
                    env_var = opt.get_env_var_name()
                    if env_var not in list_vars:
                        list_vars[env_var] = []
                    list_vars[env_var].append(opt.name)
            else:
                # Regular env vars
                if opt.current_value is not None:
                    env_var = opt.get_env_var_name()
                    if opt.var_type == "bool":
                        if opt.current_value == "1":
                            exports[env_var] = "1"
                    else:
                        exports[env_var] = opt.current_value

        # Add list-based env vars as comma-separated strings
        for env_var, values in list_vars.items():
            exports[env_var] = ",".join(values)

        return exports

    def get_unset_vars(self) -> list[str]:
        """Get list of ALL variable names that should be unset (empty or unconfigured)

        This unsets all known nvFuser variables that aren't configured, ensuring a
        clean slate when the script is sourced.
        """
        unset_vars: set[str] = set()  # Use set to avoid duplicates

        # Track which env vars have values
        env_vars_with_values: set[str] = set()

        for opt in self.all_options:
            if opt.category in ["dump", "enable", "disable"]:
                # List-based vars - only track if any are set
                if opt.current_value == "1":
                    env_vars_with_values.add(opt.get_env_var_name())
            else:
                # Regular env vars
                env_var = opt.get_env_var_name()
                if opt.current_value:
                    env_vars_with_values.add(env_var)
                else:
                    unset_vars.add(env_var)

        # Unset list vars that have no values set
        for list_var in ["NVFUSER_DUMP", "NVFUSER_ENABLE", "NVFUSER_DISABLE"]:
            if list_var not in env_vars_with_values:
                unset_vars.add(list_var)

        return sorted(list(unset_vars))

def simple_prompt_mode(config: EnvVarConfig) -> None:
    """Simple prompt-based configuration mode (no curses)."""
    # ANSI color codes
    GREEN = "\033[92m"
    CYAN = "\033[96m"
    BOLD = "\033[1m"
    RESET = "\033[0m"

    print("=" * 70)
    print("nvFuser Environment Configuration Tool - Simple Mode")
    print("=" * 70)
    print()

    for category in CATEGORY_NAMES.keys():
        if category not in config.categories:
            continue
        opts = config.categories[category]
        print(f"\n{CYAN}{BOLD}{CATEGORY_NAMES[category]}{RESET}")
        print("-" * 70)

        for opt in opts:
            # Show current value in green if set
            if opt.current_value:
                current = f"{GREEN}{opt.current_value}{RESET}"
            else:
                current = "(not set)"

            # Format the option name with type indicator
            opt_name = opt.name
            if opt.var_type == "multi":
                opt_name += " [multi]"

            print(f"\n{opt_name}:")
            print(f"  Description: {opt.description}")
            print(f"  Current: {current}")

            match opt.var_type:
                case "bool":
                    response = input("  Enable? [y/N]: ").strip().lower()
                    opt.current_value = "1" if response in ["y", "yes"] else None
                case "multi":
                    print(f"  Choices: {', '.join(repr(c) for c in opt.choices)}")
                    default = opt.choices[0] if opt.choices else ""
                    response = input(f"  Select [{default}]: ").strip()
                    opt.current_value = response if response else default
                case "int" | "string":
                    response = input("  Value: ").strip()
                    opt.current_value = response if response else None

    print("\n" + "=" * 70)
    print(f"{BOLD}Configuration Summary{RESET}")
    print("=" * 70)

    exports = config.get_env_exports()
    if not exports:
        print("No environment variables configured.")
    else:
        for var, val in exports.items():
            print(f'export {var}="{GREEN}{val}{RESET}"')

    print("\nSave configuration? [Y/n]: ", end="")
    if input().strip().lower() not in ["n", "no"]:
        unsets = config.get_unset_vars()
        save_config(exports, unsets)
        print("\nConfiguration saved to: nvfuser_env.sh")
        print("To apply: source nvfuser_env.sh")

def save_config(
    exports: dict[str, str],
    unsets: list[str] | None = None,
    filename: str = "nvfuser_env.sh",
) -> None:
    """Save configuration to shell script with both exports and unsets"""
    with open(filename, "w") as f:
        f.write("#!/bin/bash\n")
        f.write("# nvFuser Environment Configuration\n")
        f.write("# Generated by tools/configure_env.py\n\n")

        # Unset unconfigured variables first
        if unsets is not None:
            f.write("# Unset unconfigured variables\n")
            for var in sorted(unsets):
                f.write(f"unset {var}\n")
            f.write("\n")

        # Export configured variables
        if exports:
            f.write("# Export configured variables\n")
            for var, val in sorted(exports.items()):
                f.write(f'export {var}="{val}"\n')

    # Set secure permissions (600 for apply scripts, 755 for user-generated scripts)
    # If filename starts with a dot, it's likely a temporary apply script
    if os.path.basename(filename).startswith("."):
        os.chmod(filename, 0o600)  # Apply scripts: owner read/write only
    else:
        os.chmod(filename, 0o755)  # User scripts: executable by all

def generate_script_mode(config: EnvVarConfig) -> None:
    """Generate a shell script with current environment configuration."""
    exports = config.get_env_exports()

    if not exports:
        print("No environment variables currently set.")
        print("Run interactive mode to configure variables first.")
        return

    unsets = config.get_unset_vars()
    save_config(exports, unsets)
    print("\nConfiguration saved to: nvfuser_env.sh")
    print("To apply: source nvfuser_env.sh")

def try_curses_mode(config: EnvVarConfig) -> None:
    """Try to run curses-based TUI mode."""
    try:
        import curses
        from curses_ui import run_curses_ui

        curses.wrapper(lambda stdscr: run_curses_ui(stdscr, config))
    except ImportError:
        print("Error: curses module not available.")
        print("Falling back to simple mode.")
        simple_prompt_mode(config)
    except Exception as e:
        print(f"Error running TUI mode: {e}")
        print("Falling back to simple mode.")
        simple_prompt_mode(config)

def main() -> None:
    parser = argparse.ArgumentParser(
        description="Interactive nvFuser environment configuration tool",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s                    # Run interactive TUI
  %(prog)s --simple           # Run simple prompt mode
  %(prog)s --generate-script  # Generate shell script from current env
        """,
    )

    parser.add_argument(
        "--simple", action="store_true", help="Use simple prompt mode instead of TUI"
    )

    parser.add_argument(
        "--generate-script",
        action="store_true",
        help="Generate shell script from current environment",
    )

    parser.add_argument(
        "--output",
        default="nvfuser_env.sh",
        help="Output filename for generated script (default: nvfuser_env.sh)",
    )

    args = parser.parse_args()

    config = EnvVarConfig()

    if args.generate_script:
        generate_script_mode(config)
    elif args.simple:
        simple_prompt_mode(config)
    else:
        try_curses_mode(config)

if __name__ == "__main__":
    main()


EnvVarConfig

Usage:
    python tools/configure_env.py                    # Interactive TUI mode
    python tools/configure_env.py --simple           # Simple prompt mode
    python tools/configure_env.py --generate-script  # Generate shell script
"""

from __future__ import annotations

import argparse
import os
from pathlib import Path

# Load environment options from YAML
from options_loader import load_options_from_yaml, EnvVarOption

# Load configuration from YAML file
_yaml_path = Path(__file__).parent / "env_options.yaml"
CATEGORY_NAMES, ENV_VAR_DEFINITIONS = load_options_from_yaml(_yaml_path)

class EnvVarConfig:
    """Manages the current configuration state."""

    def __init__(self) -> None:
        # Use (name, env_var) tuple as key to handle options with same name
        # but different env vars (e.g., expr_simplify in DUMP vs DISABLE)
        self.options: dict[tuple[str, str | None], EnvVarOption] = {
            (opt.name, opt.env_var): opt for opt in ENV_VAR_DEFINITIONS
        }
        # Also maintain a list of all options for iteration
        self.all_options: list[EnvVarOption] = ENV_VAR_DEFINITIONS.copy()
        self.categories: dict[str, list[EnvVarOption]] = self._organize_by_category()
        self._load_current_values()

    def _organize_by_category(self) -> dict[str, list[EnvVarOption]]:
        """Organize options by category."""
        categories: dict[str, list[EnvVarOption]] = {}
        for opt in self.all_options:
            if opt.category not in categories:
                categories[opt.category] = []
            categories[opt.category].append(opt)
        return categories

    def _load_current_values(self) -> None:
        """Load current values from environment."""
        for opt in self.all_options:
            env_var_name = opt.get_env_var_name()

            if opt.category in ["dump", "enable", "disable"]:
                # These are comma-separated list values
                list_val = os.environ.get(env_var_name, "")
                if list_val:
                    list_items = [item.strip() for item in list_val.split(",")]
                    if opt.name in list_items:
                        opt.current_value = "1"
            else:
                # Regular environment variables
                if env_var_name in os.environ:
                    val = os.environ[env_var_name]
                    if opt.var_type == "bool":
                        if val.upper() in ["ON", "1", "YES", "TRUE", "Y"]:
                            opt.current_value = "1"
                    else:
                        opt.current_value = val

    def get_env_exports(self) -> dict[str, str]:
        """Generate environment variable exports based on current configuration."""
        exports: dict[str, str] = {}

        # Collect values for list-based env vars (DUMP, ENABLE, DISABLE)
        list_vars: dict[str, list[str]] = {}  # Maps env_var_name -> list of values

        for opt in self.all_options:
            if opt.category in ["dump", "enable", "disable"]:
                # List items
                if opt.current_value == "1":
                    env_var = opt.get_env_var_name()
                    if env_var not in list_vars:
                        list_vars[env_var] = []
                    list_vars[env_var].append(opt.name)
            else:
                # Regular env vars
                if opt.current_value is not None:
                    env_var = opt.get_env_var_name()
                    if opt.var_type == "bool":
                        if opt.current_value == "1":
                            exports[env_var] = "1"
                    else:
                        exports[env_var] = opt.current_value

        # Add list-based env vars as comma-separated strings
        for env_var, values in list_vars.items():
            exports[env_var] = ",".join(values)

        return exports

    def get_unset_vars(self) -> list[str]:
        """Get list of ALL variable names that should be unset (empty or unconfigured)

        This unsets all known nvFuser variables that aren't configured, ensuring a
        clean slate when the script is sourced.
        """
        unset_vars: set[str] = set()  # Use set to avoid duplicates

        # Track which env vars have values
        env_vars_with_values: set[str] = set()

        for opt in self.all_options:
            if opt.category in ["dump", "enable", "disable"]:
                # List-based vars - only track if any are set
                if opt.current_value == "1":
                    env_vars_with_values.add(opt.get_env_var_name())
            else:
                # Regular env vars
                env_var = opt.get_env_var_name()
                if opt.current_value:
                    env_vars_with_values.add(env_var)
                else:
                    unset_vars.add(env_var)

        # Unset list vars that have no values set
        for list_var in ["NVFUSER_DUMP", "NVFUSER_ENABLE", "NVFUSER_DISABLE"]:
            if list_var not in env_vars_with_values:
                unset_vars.add(list_var)

        return sorted(list(unset_vars))

def simple_prompt_mode(config: EnvVarConfig) -> None:
    """Simple prompt-based configuration mode (no curses)."""
    # ANSI color codes
    GREEN = "\033[92m"
    CYAN = "\033[96m"
    BOLD = "\033[1m"
    RESET = "\033[0m"

    print("=" * 70)
    print("nvFuser Environment Configuration Tool - Simple Mode")
    print("=" * 70)
    print()

    for category in CATEGORY_NAMES.keys():
        if category not in config.categories:
            continue
        opts = config.categories[category]
        print(f"\n{CYAN}{BOLD}{CATEGORY_NAMES[category]}{RESET}")


            if opt.category in ["dump", "enable", "disable"]:
                # List-based vars - only track if any are set
                if opt.current_value == "1":
                    env_vars_with_values.add(opt.get_env_var_name())
            else:
                # Regular env vars
                env_var = opt.get_env_var_name()
                if opt.current_value:
                    env_vars_with_values.add(env_var)
                else:
                    unset_vars.add(env_var)

        # Unset list vars that have no values set
        for list_var in ["NVFUSER_DUMP", "NVFUSER_ENABLE", "NVFUSER_DISABLE"]:
            if list_var not in env_vars_with_values:
                unset_vars.add(list_var)

        return sorted(list(unset_vars))

def simple_prompt_mode(config: EnvVarConfig) -> None:
    """Simple prompt-based configuration mode (no curses)."""
    # ANSI color codes
    GREEN = "\033[92m"
    CYAN = "\033[96m"
    BOLD = "\033[1m"
    RESET = "\033[0m"

    print("=" * 70)
    print("nvFuser Environment Configuration Tool - Simple Mode")
    print("=" * 70)
    print()

    for category in CATEGORY_NAMES.keys():
        if category not in config.categories:
            continue
        opts = config.categories[category]
        print(f"\n{CYAN}{BOLD}{CATEGORY_NAMES[category]}{RESET}")
        print("-" * 70)

        for opt in opts:
            # Show current value in green if set
            if opt.current_value:
                current = f"{GREEN}{opt.current_value}{RESET}"
            else:
                current = "(not set)"

            # Format the option name with type indicator
            opt_name = opt.name
            if opt.var_type == "multi":
                opt_name += " [multi]"

            print(f"\n{opt_name}:")
            print(f"  Description: {opt.description}")
            print(f"  Current: {current}")

            match opt.var_type:
                case "bool":
                    response = input("  Enable? [y/N]: ").strip().lower()
                    opt.current_value = "1" if response in ["y", "yes"] else None
                case "multi":
                    print(f"  Choices: {', '.join(repr(c) for c in opt.choices)}")
                    default = opt.choices[0] if opt.choices else ""
                    response = input(f"  Select [{default}]: ").strip()
                    opt.current_value = response if response else default
                case "int" | "string":
                    response = input("  Value: ").strip()
                    opt.current_value = response if response else None

    print("\n" + "=" * 70)
    print(f"{BOLD}Configuration Summary{RESET}")
    print("=" * 70)

    exports = config.get_env_exports()
    if not exports:
        print("No environment variables configured.")
    else:
        for var, val in exports.items():
            print(f'export {var}="{GREEN}{val}{RESET}"')

    print("\nSave configuration? [Y/n]: ", end="")
    if input().strip().lower() not in ["n", "no"]:
        unsets = config.get_unset_vars()
        save_config(exports, unsets)
        print("\nConfiguration saved to: nvfuser_env.sh")
        print("To apply: source nvfuser_env.sh")

def save_config(
    exports: dict[str, str],
    unsets: list[str] | None = None,
    filename: str = "nvfuser_env.sh",
) -> None:
    """Save configuration to shell script with both exports and unsets"""
    with open(filename, "w") as f:
        f.write("#!/bin/bash\n")
        f.write("# nvFuser Environment Configuration\n")
        f.write("# Generated by tools/configure_env.py\n\n")

        # Unset unconfigured variables first
        if unsets is not None:
            f.write("# Unset unconfigured variables\n")
            for var in sorted(unsets):
                f.write(f"unset {var}\n")
            f.write("\n")


    print("\n" + "=" * 70)
    print(f"{BOLD}Configuration Summary{RESET}")
    print("=" * 70)

    exports = config.get_env_exports()
    if not exports:
        print("No environment variables configured.")
    else:
        for var, val in exports.items():
            print(f'export {var}="{GREEN}{val}{RESET}"')

    print("\nSave configuration? [Y/n]: ", end="")
    if input().strip().lower() not in ["n", "no"]:
        unsets = config.get_unset_vars()
        save_config(exports, unsets)
        print("\nConfiguration saved to: nvfuser_env.sh")
        print("To apply: source nvfuser_env.sh")

def save_config(
    exports: dict[str, str],
    unsets: list[str] | None = None,
    filename: str = "nvfuser_env.sh",
) -> None:
    """Save configuration to shell script with both exports and unsets"""
    with open(filename, "w") as f:
        f.write("#!/bin/bash\n")
        f.write("# nvFuser Environment Configuration\n")
        f.write("# Generated by tools/configure_env.py\n\n")

        # Unset unconfigured variables first
        if unsets is not None:
            f.write("# Unset unconfigured variables\n")
            for var in sorted(unsets):
                f.write(f"unset {var}\n")
            f.write("\n")

        # Export configured variables
        if exports:
            f.write("# Export configured variables\n")
            for var, val in sorted(exports.items()):
                f.write(f'export {var}="{val}"\n')

    # Set secure permissions (600 for apply scripts, 755 for user-generated scripts)
    # If filename starts with a dot, it's likely a temporary apply script
    if os.path.basename(filename).startswith("."):
        os.chmod(filename, 0o600)  # Apply scripts: owner read/write only
    else:
        os.chmod(filename, 0o755)  # User scripts: executable by all

def generate_script_mode(config: EnvVarConfig) -> None:
    """Generate a shell script with current environment configuration."""
    exports = config.get_env_exports()

    if not exports:
        print("No environment variables currently set.")
        print("Run interactive mode to configure variables first.")
        return

    unsets = config.get_unset_vars()
    save_config(exports, unsets)
    print("\nConfiguration saved to: nvfuser_env.sh")
    print("To apply: source nvfuser_env.sh")

def try_curses_mode(config: EnvVarConfig) -> None:
    """Try to run curses-based TUI mode."""
    try:


        if unsets is not None:
            f.write("# Unset unconfigured variables\n")
            for var in sorted(unsets):
                f.write(f"unset {var}\n")
            f.write("\n")

        # Export configured variables
        if exports:
            f.write("# Export configured variables\n")
            for var, val in sorted(exports.items()):
                f.write(f'export {var}="{val}"\n')

    # Set secure permissions (600 for apply scripts, 755 for user-generated scripts)
    # If filename starts with a dot, it's likely a temporary apply script
    if os.path.basename(filename).startswith("."):
        os.chmod(filename, 0o600)  # Apply scripts: owner read/write only
    else:
        os.chmod(filename, 0o755)  # User scripts: executable by all

def generate_script_mode(config: EnvVarConfig) -> None:
    """Generate a shell script with current environment configuration."""
    exports = config.get_env_exports()

    if not exports:
        print("No environment variables currently set.")
        print("Run interactive mode to configure variables first.")
        return

    unsets = config.get_unset_vars()
    save_config(exports, unsets)
    print("\nConfiguration saved to: nvfuser_env.sh")
    print("To apply: source nvfuser_env.sh")

def try_curses_mode(config: EnvVarConfig) -> None:
    """Try to run curses-based TUI mode."""
    try:
        import curses
        from curses_ui import run_curses_ui

        curses.wrapper(lambda stdscr: run_curses_ui(stdscr, config))
    except ImportError:
        print("Error: curses module not available.")
        print("Falling back to simple mode.")
        simple_prompt_mode(config)
    except Exception as e:
        print(f"Error running TUI mode: {e}")
        print("Falling back to simple mode.")
        simple_prompt_mode(config)

def main() -> None:


        os.chmod(filename, 0o600)  # Apply scripts: owner read/write only
    else:
        os.chmod(filename, 0o755)  # User scripts: executable by all

def generate_script_mode(config: EnvVarConfig) -> None:
    """Generate a shell script with current environment configuration."""
    exports = config.get_env_exports()

    if not exports:
        print("No environment variables currently set.")
        print("Run interactive mode to configure variables first.")
        return

    unsets = config.get_unset_vars()
    save_config(exports, unsets)
    print("\nConfiguration saved to: nvfuser_env.sh")
    print("To apply: source nvfuser_env.sh")

def try_curses_mode(config: EnvVarConfig) -> None:
    """Try to run curses-based TUI mode."""
    try:
        import curses
        from curses_ui import run_curses_ui

        curses.wrapper(lambda stdscr: run_curses_ui(stdscr, config))
    except ImportError:
        print("Error: curses module not available.")
        print("Falling back to simple mode.")
        simple_prompt_mode(config)
    except Exception as e:
        print(f"Error running TUI mode: {e}")
        print("Falling back to simple mode.")
        simple_prompt_mode(config)

def main() -> None:
    parser = argparse.ArgumentParser(
        description="Interactive nvFuser environment configuration tool",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s                    # Run interactive TUI
  %(prog)s --simple           # Run simple prompt mode
  %(prog)s --generate-script  # Generate shell script from current env
        """,
    )

    parser.add_argument(
        "--simple", action="store_true", help="Use simple prompt mode instead of TUI"
    )

    parser.add_argument(
        "--generate-script",


main

    print("To apply: source nvfuser_env.sh")

def try_curses_mode(config: EnvVarConfig) -> None:
    """Try to run curses-based TUI mode."""
    try:
        import curses
        from curses_ui import run_curses_ui

        curses.wrapper(lambda stdscr: run_curses_ui(stdscr, config))
    except ImportError:
        print("Error: curses module not available.")
        print("Falling back to simple mode.")
        simple_prompt_mode(config)
    except Exception as e:
        print(f"Error running TUI mode: {e}")
        print("Falling back to simple mode.")
        simple_prompt_mode(config)

def main() -> None:
    parser = argparse.ArgumentParser(
        description="Interactive nvFuser environment configuration tool",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  %(prog)s                    # Run interactive TUI
  %(prog)s --simple           # Run simple prompt mode
  %(prog)s --generate-script  # Generate shell script from current env
        """,
    )

    parser.add_argument(
        "--simple", action="store_true", help="Use simple prompt mode instead of TUI"
    )

    parser.add_argument(
        "--generate-script",
        action="store_true",
        help="Generate shell script from current environment",
    )

    parser.add_argument(
        "--output",
        default="nvfuser_env.sh",
        help="Output filename for generated script (default: nvfuser_env.sh)",
    )

    args = parser.parse_args()

    config = EnvVarConfig()

    if args.generate_script:
        generate_script_mode(config)
    elif args.simple:
        simple_prompt_mode(config)
    else:
        try_curses_mode(config)

if __name__ == "__main__":
    main()


    python tools/configure_env.py --generate-script  # Generate shell script
"""

from __future__ import annotations

import argparse
import os
from pathlib import Path

# Load environment options from YAML
from options_loader import load_options_from_yaml, EnvVarOption

# Load configuration from YAML file
_yaml_path = Path(__file__).parent / "env_options.yaml"
CATEGORY_NAMES, ENV_VAR_DEFINITIONS = load_options_from_yaml(_yaml_path)

class EnvVarConfig:
    """Manages the current configuration state."""

    def __init__(self) -> None:
        # Use (name, env_var) tuple as key to handle options with same name
        # but different env vars (e.g., expr_simplify in DUMP vs DISABLE)
        self.options: dict[tuple[str, str | None], EnvVarOption] = {
            (opt.name, opt.env_var): opt for opt in ENV_VAR_DEFINITIONS
        }
        # Also maintain a list of all options for iteration
        self.all_options: list[EnvVarOption] = ENV_VAR_DEFINITIONS.copy()
        self.categories: dict[str, list[EnvVarOption]] = self._organize_by_category()
        self._load_current_values()

    def _organize_by_category(self) -> dict[str, list[EnvVarOption]]:
        """Organize options by category."""
        categories: dict[str, list[EnvVarOption]] = {}
        for opt in self.all_options:
            if opt.category not in categories:
                categories[opt.category] = []
            categories[opt.category].append(opt)
        return categories

    def _load_current_values(self) -> None:
        """Load current values from environment."""
        for opt in self.all_options:
            env_var_name = opt.get_env_var_name()

            if opt.category in ["dump", "enable", "disable"]:
                # These are comma-separated list values
                list_val = os.environ.get(env_var_name, "")
                if list_val:
                    list_items = [item.strip() for item in list_val.split(",")]


# Load configuration from YAML file
_yaml_path = Path(__file__).parent / "env_options.yaml"
CATEGORY_NAMES, ENV_VAR_DEFINITIONS = load_options_from_yaml(_yaml_path)

class EnvVarConfig:
    """Manages the current configuration state."""

    def __init__(self) -> None:
        # Use (name, env_var) tuple as key to handle options with same name
        # but different env vars (e.g., expr_simplify in DUMP vs DISABLE)
        self.options: dict[tuple[str, str | None], EnvVarOption] = {
            (opt.name, opt.env_var): opt for opt in ENV_VAR_DEFINITIONS
        }
        # Also maintain a list of all options for iteration
        self.all_options: list[EnvVarOption] = ENV_VAR_DEFINITIONS.copy()
        self.categories: dict[str, list[EnvVarOption]] = self._organize_by_category()
        self._load_current_values()

    def _organize_by_category(self) -> dict[str, list[EnvVarOption]]:
        """Organize options by category."""
        categories: dict[str, list[EnvVarOption]] = {}
        for opt in self.all_options:
            if opt.category not in categories:
                categories[opt.category] = []
            categories[opt.category].append(opt)
        return categories

    def _load_current_values(self) -> None:
        """Load current values from environment."""
        for opt in self.all_options:
            env_var_name = opt.get_env_var_name()

            if opt.category in ["dump", "enable", "disable"]:
                # These are comma-separated list values
                list_val = os.environ.get(env_var_name, "")
                if list_val:
                    list_items = [item.strip() for item in list_val.split(",")]
                    if opt.name in list_items:
                        opt.current_value = "1"
            else:
                # Regular environment variables
                if env_var_name in os.environ:
                    val = os.environ[env_var_name]
                    if opt.var_type == "bool":
                        if val.upper() in ["ON", "1", "YES", "TRUE", "Y"]:
                            opt.current_value = "1"


    def __init__(self) -> None:
        # Use (name, env_var) tuple as key to handle options with same name
        # but different env vars (e.g., expr_simplify in DUMP vs DISABLE)
        self.options: dict[tuple[str, str | None], EnvVarOption] = {
            (opt.name, opt.env_var): opt for opt in ENV_VAR_DEFINITIONS
        }
        # Also maintain a list of all options for iteration
        self.all_options: list[EnvVarOption] = ENV_VAR_DEFINITIONS.copy()
        self.categories: dict[str, list[EnvVarOption]] = self._organize_by_category()
        self._load_current_values()

    def _organize_by_category(self) -> dict[str, list[EnvVarOption]]:
        """Organize options by category."""
        categories: dict[str, list[EnvVarOption]] = {}
        for opt in self.all_options:
            if opt.category not in categories:
                categories[opt.category] = []
            categories[opt.category].append(opt)
        return categories

    def _load_current_values(self) -> None:
        """Load current values from environment."""
        for opt in self.all_options:
            env_var_name = opt.get_env_var_name()

            if opt.category in ["dump", "enable", "disable"]:
                # These are comma-separated list values
                list_val = os.environ.get(env_var_name, "")
                if list_val:
                    list_items = [item.strip() for item in list_val.split(",")]
                    if opt.name in list_items:
                        opt.current_value = "1"
            else:
                # Regular environment variables
                if env_var_name in os.environ:
                    val = os.environ[env_var_name]
                    if opt.var_type == "bool":
                        if val.upper() in ["ON", "1", "YES", "TRUE", "Y"]:
                            opt.current_value = "1"
                    else:
                        opt.current_value = val

    def get_env_exports(self) -> dict[str, str]:
        """Generate environment variable exports based on current configuration."""
        exports: dict[str, str] = {}

        # Collect values for list-based env vars (DUMP, ENABLE, DISABLE)
        list_vars: dict[str, list[str]] = {}  # Maps env_var_name -> list of values

        for opt in self.all_options:
            if opt.category in ["dump", "enable", "disable"]:
                # List items
                if opt.current_value == "1":
                    env_var = opt.get_env_var_name()
                    if env_var not in list_vars:
                        list_vars[env_var] = []
                    list_vars[env_var].append(opt.name)
            else:
                # Regular env vars
                if opt.current_value is not None:
                    env_var = opt.get_env_var_name()


        for opt in self.all_options:
            env_var_name = opt.get_env_var_name()

            if opt.category in ["dump", "enable", "disable"]:
                # These are comma-separated list values
                list_val = os.environ.get(env_var_name, "")
                if list_val:
                    list_items = [item.strip() for item in list_val.split(",")]
                    if opt.name in list_items:
                        opt.current_value = "1"
            else:
                # Regular environment variables
                if env_var_name in os.environ:
                    val = os.environ[env_var_name]
                    if opt.var_type == "bool":
                        if val.upper() in ["ON", "1", "YES", "TRUE", "Y"]:
                            opt.current_value = "1"
                    else:
                        opt.current_value = val

    def get_env_exports(self) -> dict[str, str]:
        """Generate environment variable exports based on current configuration."""
        exports: dict[str, str] = {}

        # Collect values for list-based env vars (DUMP, ENABLE, DISABLE)
        list_vars: dict[str, list[str]] = {}  # Maps env_var_name -> list of values

        for opt in self.all_options:
            if opt.category in ["dump", "enable", "disable"]:
                # List items
                if opt.current_value == "1":
                    env_var = opt.get_env_var_name()
                    if env_var not in list_vars:
                        list_vars[env_var] = []
                    list_vars[env_var].append(opt.name)
            else:
                # Regular env vars
                if opt.current_value is not None:
                    env_var = opt.get_env_var_name()
                    if opt.var_type == "bool":
                        if opt.current_value == "1":
                            exports[env_var] = "1"
                    else:
                        exports[env_var] = opt.current_value

        # Add list-based env vars as comma-separated strings
        for env_var, values in list_vars.items():
            exports[env_var] = ",".join(values)

        return exports

    def get_unset_vars(self) -> list[str]:
        """Get list of ALL variable names that should be unset (empty or unconfigured)

        This unsets all known nvFuser variables that aren't configured, ensuring a
        clean slate when the script is sourced.
        """
        unset_vars: set[str] = set()  # Use set to avoid duplicates

        # Track which env vars have values
        env_vars_with_values: set[str] = set()

        for opt in self.all_options:
            if opt.category in ["dump", "enable", "disable"]:
                # List-based vars - only track if any are set
                if opt.current_value == "1":
                    env_vars_with_values.add(opt.get_env_var_name())
            else:
                # Regular env vars
                env_var = opt.get_env_var_name()


                    env_var = opt.get_env_var_name()
                    if env_var not in list_vars:
                        list_vars[env_var] = []
                    list_vars[env_var].append(opt.name)
            else:
                # Regular env vars
                if opt.current_value is not None:
                    env_var = opt.get_env_var_name()
                    if opt.var_type == "bool":
                        if opt.current_value == "1":
                            exports[env_var] = "1"
                    else:
                        exports[env_var] = opt.current_value

        # Add list-based env vars as comma-separated strings
        for env_var, values in list_vars.items():
            exports[env_var] = ",".join(values)

        return exports

    def get_unset_vars(self) -> list[str]:
        """Get list of ALL variable names that should be unset (empty or unconfigured)

        This unsets all known nvFuser variables that aren't configured, ensuring a
        clean slate when the script is sourced.
        """
        unset_vars: set[str] = set()  # Use set to avoid duplicates

        # Track which env vars have values
        env_vars_with_values: set[str] = set()

        for opt in self.all_options:
            if opt.category in ["dump", "enable", "disable"]:
                # List-based vars - only track if any are set
                if opt.current_value == "1":
                    env_vars_with_values.add(opt.get_env_var_name())
            else:
                # Regular env vars
                env_var = opt.get_env_var_name()
                if opt.current_value:
                    env_vars_with_values.add(env_var)
                else:
                    unset_vars.add(env_var)

        # Unset list vars that have no values set
        for list_var in ["NVFUSER_DUMP", "NVFUSER_ENABLE", "NVFUSER_DISABLE"]:
            if list_var not in env_vars_with_values:
                unset_vars.add(list_var)

        return sorted(list(unset_vars))

def simple_prompt_mode(config: EnvVarConfig) -> None:
    """Simple prompt-based configuration mode (no curses)."""
    # ANSI color codes
    GREEN = "\033[92m"
    CYAN = "\033[96m"
    BOLD = "\033[1m"
    RESET = "\033[0m"

    print("=" * 70)
    print("nvFuser Environment Configuration Tool - Simple Mode")
    print("=" * 70)
    print()

    for category in CATEGORY_NAMES.keys():
        if category not in config.categories:
            continue
        opts = config.categories[category]
        print(f"\n{CYAN}{BOLD}{CATEGORY_NAMES[category]}{RESET}")


import argparse
import concurrent.futures
import json
import logging
import os
import re
import shutil
import subprocess
import sys
import sysconfig
import time
from enum import Enum
from pathlib import Path
from typing import Any, List, NamedTuple, Optional, Pattern

# Nvfuser directory root
result = subprocess.run(
    ["git", "rev-parse", "--show-toplevel"],
    stdout=subprocess.PIPE,
    check=True,
)
NVFUSER_ROOT = result.stdout.decode("utf-8").strip()
IS_WINDOWS: bool = os.name == "nt"

# Returns '/usr/local/include/python<version number>'
def get_python_include_dir() -> str:
    return sysconfig.get_paths()["include"]

def eprint(*args: Any, **kwargs: Any) -> None:
    print(*args, file=sys.stderr, flush=True, **kwargs)

class LintSeverity(str, Enum):
    ERROR = "error"
    WARNING = "warning"
    ADVICE = "advice"
    DISABLED = "disabled"

class LintMessage(NamedTuple):
    path: Optional[str]
    line: Optional[int]
    char: Optional[int]
    code: str
    severity: LintSeverity
    name: str
    original: Optional[str]
    replacement: Optional[str]
    description: Optional[str]

def as_posix(name: str) -> str:
    return name.replace("\\", "/") if IS_WINDOWS else name

# c10/core/DispatchKey.cpp:281:26: error: 'k' used after it was moved [bugprone-use-after-move]
RESULTS_RE: Pattern[str] = re.compile(
    r"""(?mx)
    ^
    (?P<file>.*?):
    (?P<line>\d+):
    (?:(?P<column>-?\d+):)?
    \s(?P<severity>\S+?):?
    \s(?P<message>.*)
    \s(?P<code>\[.*\])
    $
    """
)

def run_command(
    args: List[str],
) -> subprocess.CompletedProcess[str]:
    logging.debug("$ %s", " ".join(args))
    start_time = time.monotonic()
    try:
        return subprocess.run(
            args,
            capture_output=True,
            check=True,
            text=True,
        )
    finally:
        end_time = time.monotonic()
        logging.debug("took %dms", (end_time - start_time) * 1000)

# Severity is either "error" or "note":
# https://github.com/python/mypy/blob/8b47a032e1317fb8e3f9a818005a6b63e9bf0311/mypy/errors.py#L46-L47
severities = {
    "error": LintSeverity.ERROR,
    "warning": LintSeverity.WARNING,
}

def clang_search_dirs() -> List[str]:
    # Compilers are ordered based on fallback preference
    # We pick the first one that is available on the system
    compilers = ["clang", "gcc", "cpp", "cc"]
    compilers = [c for c in compilers if shutil.which(c) is not None]
    if len(compilers) == 0:
        raise RuntimeError(f"None of {compilers} were found")
    compiler = compilers[0]

    result = subprocess.run(
        [compiler, "-E", "-x", "c++", "-", "-v"],
        stdin=subprocess.DEVNULL,
        capture_output=True,
        check=True,
    )
    stderr = result.stderr.decode().strip().split("\n")
    search_start = r"#include.*search starts here:"
    search_end = r"End of search list."

    append_path = False
    search_paths = []
    for line in stderr:
        if re.match(search_start, line):
            if append_path:
                continue
            else:
                append_path = True
        elif re.match(search_end, line):
            break
        elif append_path:
            search_paths.append(line.strip())

    return search_paths

# NVFUSER_USE_PCH=ON somehow requires the CUDA include dir e.g.
# "/usr/local/cuda-13.0/targets/x86_64-linux/include". I'll try to add that in
# a future PR.
include_dir = [
    get_python_include_dir(),
    os.path.join(NVFUSER_ROOT, "third_party/pybind11/include"),
] + clang_search_dirs()

include_args = ["--extra-arg=-Wno-unknown-warning-option"]
for dir in include_dir:
    include_args += ["--extra-arg", f"-I{dir}"]

def check_file(
    filename: str,
    binary: str,
    build_dir: Path,
) -> List[LintMessage]:
    try:
        proc = run_command(
            [binary, f"-p={build_dir}", *include_args, filename],
        )
    except subprocess.CalledProcessError as err:
        return [
            LintMessage(
                path=filename,
                line=None,
                char=None,
                code="CLANGTIDY",
                severity=LintSeverity.ERROR,
                name="command-failed",
                original=None,
                replacement=None,
                description=(
                    f"Failed due to {err}\n\nstdout:\n{err.stdout}\nstderr:\n{err.stderr}"
                ),
            )
        ]
    lint_messages = []
    try:
        # Change the current working directory to the build directory, since
        # clang-tidy will report files relative to the build directory.
        saved_cwd = os.getcwd()
        os.chdir(build_dir)

        for match in RESULTS_RE.finditer(proc.stdout):
            # Convert the reported path to an absolute path.
            abs_path = str(Path(match["file"]).resolve())
            message = LintMessage(
                path=abs_path,
                name=match["code"],
                description=match["message"],
                line=int(match["line"]),
                char=int(match["column"])
                if match["column"] is not None and not match["column"].startswith("-")
                else None,
                code="CLANGTIDY",
                severity=severities.get(match["severity"], LintSeverity.ERROR),
                original=None,
                replacement=None,
            )
            lint_messages.append(message)
    finally:
        os.chdir(saved_cwd)

    return lint_messages

def main() -> None:
    parser = argparse.ArgumentParser(
        description="clang-tidy wrapper linter.",
        fromfile_prefix_chars="@",
    )
    parser.add_argument(
        "--build-dir",
        "--build_dir",
        required=True,
        help=(
            "Where the compile_commands.json file is located. "
            "Gets passed to clang-tidy -p"
        ),
    )
    parser.add_argument(
        "--verbose",
        action="store_true",
        help="verbose logging",
    )
    parser.add_argument(
        "filenames",
        nargs="+",
        help="paths to lint",
    )
    args = parser.parse_args()

    logging.basicConfig(
        format="<%(threadName)s:%(levelname)s> %(message)s",
        level=logging.NOTSET
        if args.verbose
        else logging.DEBUG
        if len(args.filenames) < 1000
        else logging.INFO,
        stream=sys.stderr,
    )

    try:
        llvm_bindir = subprocess.check_output(
            ["llvm-config", "--bindir"], text=True
        ).strip()
    except FileNotFoundError:
        err_msg = LintMessage(
            path="<none>",
            line=None,
            char=None,
            code="CLANGTIDY",
            severity=LintSeverity.ERROR,
            name="command-failed",
            original=None,
            replacement=None,
            description="llvm-config doesn't exist (is LLVM installed?).",
        )
        print(json.dumps(err_msg._asdict()), flush=True)
        exit(0)

    binary_path = os.path.join(llvm_bindir, "clang-tidy")
    if not os.path.exists(binary_path):
        err_msg = LintMessage(
            path="<none>",
            line=None,
            char=None,
            code="CLANGTIDY",
            severity=LintSeverity.ERROR,
            name="command-failed",
            original=None,
            replacement=None,
            description=f"Could not find clang-tidy binary at {binary_path} (is LLVM installed?)",
        )
        print(json.dumps(err_msg._asdict()), flush=True)
        exit(0)

    abs_build_dir = Path(args.build_dir).resolve()

    with concurrent.futures.ThreadPoolExecutor(
        max_workers=os.cpu_count(),
        thread_name_prefix="Thread",
    ) as executor:
        futures = {
            executor.submit(
                check_file,
                filename,
                binary_path,
                abs_build_dir,
            ): filename
            for filename in args.filenames
        }
        for future in concurrent.futures.as_completed(futures):
            try:
                for lint_message in future.result():
                    print(json.dumps(lint_message._asdict()), flush=True)
            except Exception:
                logging.critical('Failed at "%s".', futures[future])
                raise

if __name__ == "__main__":
    main()


import shutil
import subprocess
import sys
import sysconfig
import time
from enum import Enum
from pathlib import Path
from typing import Any, List, NamedTuple, Optional, Pattern

# Nvfuser directory root
result = subprocess.run(
    ["git", "rev-parse", "--show-toplevel"],
    stdout=subprocess.PIPE,
    check=True,
)
NVFUSER_ROOT = result.stdout.decode("utf-8").strip()
IS_WINDOWS: bool = os.name == "nt"

# Returns '/usr/local/include/python<version number>'
def get_python_include_dir() -> str:
    return sysconfig.get_paths()["include"]

def eprint(*args: Any, **kwargs: Any) -> None:
    print(*args, file=sys.stderr, flush=True, **kwargs)

class LintSeverity(str, Enum):
    ERROR = "error"
    WARNING = "warning"
    ADVICE = "advice"
    DISABLED = "disabled"

class LintMessage(NamedTuple):
    path: Optional[str]
    line: Optional[int]
    char: Optional[int]
    code: str
    severity: LintSeverity
    name: str


eprint

import time
from enum import Enum
from pathlib import Path
from typing import Any, List, NamedTuple, Optional, Pattern

# Nvfuser directory root
result = subprocess.run(
    ["git", "rev-parse", "--show-toplevel"],
    stdout=subprocess.PIPE,
    check=True,
)
NVFUSER_ROOT = result.stdout.decode("utf-8").strip()
IS_WINDOWS: bool = os.name == "nt"

# Returns '/usr/local/include/python<version number>'
def get_python_include_dir() -> str:
    return sysconfig.get_paths()["include"]

def eprint(*args: Any, **kwargs: Any) -> None:
    print(*args, file=sys.stderr, flush=True, **kwargs)

class LintSeverity(str, Enum):
    ERROR = "error"
    WARNING = "warning"
    ADVICE = "advice"
    DISABLED = "disabled"

class LintMessage(NamedTuple):
    path: Optional[str]
    line: Optional[int]
    char: Optional[int]
    code: str
    severity: LintSeverity
    name: str
    original: Optional[str]
    replacement: Optional[str]
    description: Optional[str]


LintSeverity

# Nvfuser directory root
result = subprocess.run(
    ["git", "rev-parse", "--show-toplevel"],
    stdout=subprocess.PIPE,
    check=True,
)
NVFUSER_ROOT = result.stdout.decode("utf-8").strip()
IS_WINDOWS: bool = os.name == "nt"

# Returns '/usr/local/include/python<version number>'
def get_python_include_dir() -> str:
    return sysconfig.get_paths()["include"]

def eprint(*args: Any, **kwargs: Any) -> None:
    print(*args, file=sys.stderr, flush=True, **kwargs)

class LintSeverity(str, Enum):
    ERROR = "error"
    WARNING = "warning"
    ADVICE = "advice"
    DISABLED = "disabled"

class LintMessage(NamedTuple):
    path: Optional[str]
    line: Optional[int]
    char: Optional[int]
    code: str
    severity: LintSeverity
    name: str
    original: Optional[str]
    replacement: Optional[str]
    description: Optional[str]

def as_posix(name: str) -> str:
    return name.replace("\\", "/") if IS_WINDOWS else name

# c10/core/DispatchKey.cpp:281:26: error: 'k' used after it was moved [bugprone-use-after-move]
RESULTS_RE: Pattern[str] = re.compile(


LintMessage

NVFUSER_ROOT = result.stdout.decode("utf-8").strip()
IS_WINDOWS: bool = os.name == "nt"

# Returns '/usr/local/include/python<version number>'
def get_python_include_dir() -> str:
    return sysconfig.get_paths()["include"]

def eprint(*args: Any, **kwargs: Any) -> None:
    print(*args, file=sys.stderr, flush=True, **kwargs)

class LintSeverity(str, Enum):
    ERROR = "error"
    WARNING = "warning"
    ADVICE = "advice"
    DISABLED = "disabled"

class LintMessage(NamedTuple):
    path: Optional[str]
    line: Optional[int]
    char: Optional[int]
    code: str
    severity: LintSeverity
    name: str
    original: Optional[str]
    replacement: Optional[str]
    description: Optional[str]

def as_posix(name: str) -> str:
    return name.replace("\\", "/") if IS_WINDOWS else name

# c10/core/DispatchKey.cpp:281:26: error: 'k' used after it was moved [bugprone-use-after-move]
RESULTS_RE: Pattern[str] = re.compile(
    r"""(?mx)
    ^
    (?P<file>.*?):
    (?P<line>\d+):
    (?:(?P<column>-?\d+):)?
    \s(?P<severity>\S+?):?
    \s(?P<message>.*)
    \s(?P<code>\[.*\])
    $
    """
)


class LintSeverity(str, Enum):
    ERROR = "error"
    WARNING = "warning"
    ADVICE = "advice"
    DISABLED = "disabled"

class LintMessage(NamedTuple):
    path: Optional[str]
    line: Optional[int]
    char: Optional[int]
    code: str
    severity: LintSeverity
    name: str
    original: Optional[str]
    replacement: Optional[str]
    description: Optional[str]

def as_posix(name: str) -> str:
    return name.replace("\\", "/") if IS_WINDOWS else name

# c10/core/DispatchKey.cpp:281:26: error: 'k' used after it was moved [bugprone-use-after-move]
RESULTS_RE: Pattern[str] = re.compile(
    r"""(?mx)
    ^
    (?P<file>.*?):
    (?P<line>\d+):
    (?:(?P<column>-?\d+):)?
    \s(?P<severity>\S+?):?
    \s(?P<message>.*)
    \s(?P<code>\[.*\])
    $
    """
)

def run_command(
    args: List[str],
) -> subprocess.CompletedProcess[str]: