#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# Fenrir TTY screen reader
# By Chrys, Storm Dragon, and contributors.
# attrib:
# http://rampex.ihep.su/Linux/linux_howto/html/tutorials/mini/Colour-ls-6.html
# 0 = black, 1 = blue, 2 = green, 3 = cyan, 4 = red, 5 = purple, 6 = brown/yellow, 7 = white.
# https://github.com/jwilk/vcsapeek/blob/master/linuxvt.py
# blink = 5 if attr & 1 else 0
# bold = 1 if attr & 16 else 0

import fcntl
import glob
import os
import select
import subprocess
import termios
import time
from array import array
from fcntl import ioctl
from struct import pack
from struct import unpack
from struct import unpack_from

import dbus

from fenrirscreenreader.core import debug
from fenrirscreenreader.core.eventData import FenrirEventType
from fenrirscreenreader.core.screenDriver import ScreenDriver as screenDriver
from fenrirscreenreader.utils import screen_utils


class driver(screenDriver):
    """Linux VCSA (Virtual Console Screen Access) driver for Fenrir screen reader.
    
    This driver provides access to Linux virtual consoles (TTYs) through the VCSA
    interface, allowing real-time monitoring of screen content and cursor position.
    It supports both text content extraction and color/attribute detection.
    
    The driver monitors multiple virtual consoles simultaneously and can detect:
    - Screen content changes (text updates)
    - Cursor movement
    - TTY switching
    - Text attributes (colors, bold, etc.)
    - Session information via D-Bus/logind
    
    Attributes:
        ListSessions: D-Bus method for listing login sessions
        sysBus: D-Bus system bus connection
        charmap: Character mapping for text decoding
        bgColorValues: Background color value mappings
        fgColorValues: Foreground color value mappings
        hichar: High character mask for Unicode support
    """
    def __init__(self):
        screenDriver.__init__(self)
        self.ListSessions = None
        self.sysBus = None
        self.charmap = {}
        self.bgColorValues = {
            0: "black",
            1: "blue",
            2: "green",
            3: "cyan",
            4: "red",
            5: "magenta",
            6: "brown/yellow",
            7: "white",
        }
        self.fgColorValues = {
            0: "black",
            1: "blue",
            2: "green",
            3: "cyan",
            4: "red",
            5: "magenta",
            6: "brown/yellow",
            7: "light gray",
            8: "dark gray",
            9: "light blue",
            10: "light green",
            11: "light cyan",
            12: "light red",
            13: "light magenta",
            14: "light yellow",
            15: "white",
        }
        self.hichar = None
        try:
            # set workaround for paste clipboard -> inject_text_to_screen
            subprocess.run(
                ["sysctl", "dev.tty.legacy_tiocsti=1"],
                check=False,
                capture_output=True,
                timeout=5,
            )
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "vcsaDriver shutdown: Error running fgconsole: " + str(e),
                debug.DebugLevel.ERROR,
            )

    def initialize(self, environment):
        """Initialize the VCSA driver with the given environment.
        
        Sets up default attributes, starts the screen monitoring watchdog process,
        and prepares the driver for screen content monitoring.
        
        Args:
            environment: The Fenrir environment dictionary containing runtime managers
                       and configuration settings.
        """
        self.env = environment
        self.env["runtime"]["AttributeManager"].append_default_attributes(
            [
                self.fgColorValues[7],  # fg
                self.bgColorValues[0],  # bg
                False,  # bold
                False,  # italics
                False,  # underscore
                False,  # strikethrough
                False,  # reverse
                False,  # blink
                "default",  # fontsize
                "default",  # fontfamily
            ]
        )  # end attribute )
        # Use threading instead of multiprocessing to avoid pickle issues
        # with self.env (which contains unpicklable file handles)
        self.env["runtime"]["ProcessManager"].add_custom_event_thread(
            self.update_watchdog, multiprocess=False
        )

    def get_curr_screen(self):
        """Get the currently active TTY number.
        
        Reads from /sys/devices/virtual/tty/tty0/active to determine which
        virtual console is currently active and updates the environment.
        
        Updates:
            env['screen']['oldTTY']: Previous TTY number
            env['screen']['newTTY']: Current TTY number
        """
        self.env["screen"]["oldTTY"] = self.env["screen"]["newTTY"]
        try:
            with open(
                "/sys/devices/virtual/tty/tty0/active", "r"
            ) as currScreenFile:
                self.env["screen"]["newTTY"] = str(currScreenFile.read()[3:-1])
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                str(e), debug.DebugLevel.ERROR
            )

    def inject_text_to_screen(self, text, screen=None):
        """Inject text into the specified screen as if typed by user.
        
        Uses the TIOCSTI ioctl to simulate keystrokes on the target TTY.
        This is primarily used for clipboard paste functionality.
        
        Args:
            text (str): Text to inject into the screen
            screen (str, optional): Target screen device (e.g., '/dev/tty1').
                                  If None, uses current TTY.
        
        Note:
            Requires appropriate permissions and may need legacy_tiocsti=1
            kernel parameter on newer systems.
        """
        use_screen = "/dev/tty" + self.env["screen"]["newTTY"]
        if screen is not None:
            use_screen = screen
        with open(use_screen, "w") as fd:
            text_bytes = text.encode('utf-8')
            for byte in text_bytes:
                fcntl.ioctl(fd, termios.TIOCSTI, bytes([byte]))

    def get_session_information(self):
        """Retrieve session information via D-Bus logind interface.
        
        Connects to systemd-logind to gather information about active sessions,
        including session types and TTY assignments. This helps identify which
        screens should be automatically ignored (e.g., X11 sessions).
        
        Updates:
            env['screen']['autoIgnoreScreens']: List of screens to ignore
            env['general']['curr_user']: Current user for active session
            env['general']['prev_user']: Previous user
        
        Note:
            Gracefully handles cases where logind is not available.
        """
        self.env["screen"]["autoIgnoreScreens"] = []
        try:
            if not self.sysBus:
                self.sysBus = dbus.SystemBus()
                obj = self.sysBus.get_object(
                    "org.freedesktop.login1", "/org/freedesktop/login1"
                )
                inf = dbus.Interface(obj, "org.freedesktop.login1.Manager")
                self.ListSessions = inf.get_dbus_method("ListSessions")
            sessions = self.ListSessions()

            for session in sessions:
                obj = self.sysBus.get_object(
                    "org.freedesktop.login1", session[4]
                )
                inf = dbus.Interface(obj, "org.freedesktop.DBus.Properties")
                session_type = inf.Get(
                    "org.freedesktop.login1.Session", "Type"
                )
                screen = str(inf.Get("org.freedesktop.login1.Session", "VTNr"))
                if screen == "":
                    screen = str(
                        inf.Get("org.freedesktop.login1.Session", "TTY")
                    )
                    screen = screen[screen.upper().find("TTY") + 3 :]
                if screen == "":
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "No TTY found for session:" + session[4],
                        debug.DebugLevel.ERROR,
                    )
                    return
                if session_type.upper() != "TTY":
                    self.env["screen"]["autoIgnoreScreens"] += [screen]
                if screen == self.env["screen"]["newTTY"]:
                    if self.env["general"]["curr_user"] != session[2]:
                        self.env["general"]["prev_user"] = self.env["general"][
                            "curr_user"
                        ]
                        self.env["general"]["curr_user"] = session[2]
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "get_session_information: Maybe no LoginD:" + str(e),
                debug.DebugLevel.ERROR,
            )
        # self.env['runtime']['DebugManager'].write_debug_out('get_session_information:'  + str(self.env['screen']['autoIgnoreScreens']) + ' ' + str(self.env['general'])  ,debug.DebugLevel.INFO)

    def read_file(self, file):
        """Read content from a file handle with error recovery.
        
        Attempts to read the entire file content, falling back to
        line-by-line reading if the initial read fails. This is used
        for reading VCSA/VCSU device files.
        
        Args:
            file: Open file handle to read from
            
        Returns:
            bytes: File content as bytes
        """
        d = b""
        file.seek(0)
        try:
            d = file.read()
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "vcsaDriver get_screen_text: Error reading file: " + str(e),
                debug.DebugLevel.ERROR,
            )
            file.seek(0)
            while True:
                # Read from file
                try:
                    d += file.readline(1)
                    if not d:
                        break
                except Exception as e:
                    break
        return d

    def update_watchdog(self, active, event_queue):
        """Main watchdog loop for monitoring screen changes.
        
        This is the core monitoring function that runs in a separate process.
        It uses epoll to efficiently monitor multiple VCSA devices and the
        active TTY file for changes. When changes are detected, it generates
        appropriate events for the main Fenrir process.
        
        The watchdog monitors:
        - Screen content changes (text updates)
        - TTY switches (screen changes)
        - Cursor position changes
        
        Args:
            active: Shared boolean value controlling the watchdog loop
            event_queue: Queue for sending events to the main process
            
        Events Generated:
            - FenrirEventType.screen_changed: When switching TTYs
            - FenrirEventType.screen_update: When screen content changes
            
        Note:
            This method runs in a multiprocess context and includes comprehensive
            cleanup of file handles in the finally block.
        """
        vcsa = {}
        vcsu = {}
        tty = None
        watchdog = None
        try:
            use_vcsu = os.access("/dev/vcsu", os.R_OK)
            vcsa_devices = glob.glob("/dev/vcsa*")
            vcsu_devices = None
            last_screen_content = b""

            # Open TTY file with proper cleanup
            tty = open("/sys/devices/virtual/tty/tty0/active", "r")
            curr_screen = str(tty.read()[3:-1])
            old_screen = curr_screen

            # Open VCSA devices with proper cleanup tracking
            for vcsaDev in vcsa_devices:
                index = str(vcsaDev[9:])
                vcsa[index] = open(vcsaDev, "rb")
                if index == curr_screen:
                    last_screen_content = self.read_file(vcsa[index])

            # Open VCSU devices if available
            if use_vcsu:
                vcsu_devices = glob.glob("/dev/vcsu*")
                for vcsuDev in vcsu_devices:
                    index = str(vcsuDev[9:])
                    vcsu[index] = open(vcsuDev, "rb")

            self.update_char_map(curr_screen)
            watchdog = select.epoll()
            watchdog.register(
                vcsa[curr_screen], select.POLLPRI | select.POLLERR
            )
            watchdog.register(tty, select.POLLPRI | select.POLLERR)
            while active.value:
                changes = watchdog.poll(1)
                for change in changes:
                    fileno = change[0]
                    event = change[1]
                    if fileno == tty.fileno():
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "ScreenChange", debug.DebugLevel.INFO
                        )
                        tty.seek(0)
                        curr_screen = str(tty.read()[3:-1])
                        if curr_screen != old_screen:
                            try:
                                watchdog.unregister(vcsa[old_screen])
                            except Exception as e:
                                self.env["runtime"][
                                    "DebugManager"
                                ].write_debug_out(
                                    "vcsaDriver update_watchdog: Error unregistering watchdog: "
                                    + str(e),
                                    debug.DebugLevel.ERROR,
                                )
                            try:
                                watchdog.register(
                                    vcsa[curr_screen],
                                    select.POLLPRI | select.POLLERR,
                                )
                            except Exception as e:
                                self.env["runtime"][
                                    "DebugManager"
                                ].write_debug_out(
                                    "vcsaDriver update_watchdog: Error registering watchdog: "
                                    + str(e),
                                    debug.DebugLevel.ERROR,
                                )
                            self.update_char_map(curr_screen)
                            old_screen = curr_screen
                            try:
                                vcsa[curr_screen].seek(0)
                                last_screen_content = self.read_file(
                                    vcsa[curr_screen]
                                )
                            except Exception as e:
                                self.env["runtime"][
                                    "DebugManager"
                                ].write_debug_out(
                                    "vcsaDriver update_watchdog: Error reading screen content: "
                                    + str(e),
                                    debug.DebugLevel.ERROR,
                                )
                            vcsu_content = None
                            if use_vcsu:
                                vcsu[curr_screen].seek(0)
                                vcsu_content = self.read_file(
                                    vcsu[curr_screen]
                                )
                            event_queue.put(
                                {
                                    "Type": FenrirEventType.screen_changed,
                                    "data": self.create_screen_event_data(
                                        curr_screen,
                                        last_screen_content,
                                        vcsu_content,
                                    ),
                                }
                            )
                    else:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "screen_update", debug.DebugLevel.INFO
                        )
                        vcsa[curr_screen].seek(0)
                        time.sleep(0.01)
                        dirty_content = self.read_file(vcsa[curr_screen])
                        screen_content = dirty_content
                        vcsu_content = None
                        timeout = time.time()
                        # error case
                        if screen_content == b"":
                            continue
                        if last_screen_content == b"":
                            last_screen_content = screen_content
                        if (
                            abs(
                                int(screen_content[2])
                                - int(last_screen_content[2])
                            )
                            in [1, 2]
                        ) and (
                            int(screen_content[3])
                            == int(last_screen_content[3])
                        ):
                            # Skip X Movement
                            pass
                        elif (
                            abs(
                                int(screen_content[3])
                                - int(last_screen_content[3])
                            )
                            in [1]
                        ) and (
                            int(screen_content[2])
                            == int(last_screen_content[2])
                        ):
                            # Skip Y Movement
                            pass
                        else:
                            # anything else? wait for completion
                            while True:
                                screen_content = dirty_content
                                time.sleep(0.02)
                                # r,_,_ = select.select([vcsa[curr_screen]], [], [], 0.07)
                                # if not vcsa[curr_screen] in r:
                                #    break
                                vcsa[curr_screen].seek(0)
                                dirty_content = self.read_file(
                                    vcsa[curr_screen]
                                )
                                if screen_content == dirty_content:
                                    break
                                if time.time() - timeout >= 0.1:
                                    screen_content = dirty_content
                                    break
                        if use_vcsu:
                            vcsu[curr_screen].seek(0)
                            vcsu_content = self.read_file(vcsu[curr_screen])
                        last_screen_content = screen_content
                        event_queue.put(
                            {
                                "Type": FenrirEventType.screen_update,
                                "data": self.create_screen_event_data(
                                    curr_screen, screen_content, vcsu_content
                                ),
                            }
                        )
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "VCSA:update_watchdog:" + str(e), debug.DebugLevel.ERROR
            )
            time.sleep(0.2)
        finally:
            # Clean up all file handles
            try:
                if watchdog:
                    watchdog.close()
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "vcsaDriver update_watchdog: Error closing watchdog: "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
            try:
                if tty:
                    tty.close()
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "vcsaDriver shutdown: Error closing TTY: " + str(e),
                    debug.DebugLevel.ERROR,
                )
            for handle in vcsa.values():
                try:
                    handle.close()
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "vcsaDriver shutdown: Error closing VCSA handle: "
                        + str(e),
                        debug.DebugLevel.ERROR,
                    )
            for handle in vcsu.values():
                try:
                    handle.close()
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "vcsaDriver shutdown: Error closing VCSU handle: "
                        + str(e),
                        debug.DebugLevel.ERROR,
                    )

    def create_screen_event_data(self, screen, vcsaContent, vcsu_content=None):
        """Create standardized screen event data from VCSA content.
        
        Processes raw VCSA bytes into a structured event data dictionary
        containing screen dimensions, cursor position, text content, and
        color attributes.
        
        Args:
            screen (str): TTY number (e.g., '1' for tty1)
            vcsaContent (bytes): Raw VCSA device content
            vcsu_content (bytes, optional): VCSU content for Unicode support
            
        Returns:
            dict: Event data with keys:
                - bytes: Raw VCSA content
                - lines: Screen height
                - columns: Screen width  
                - textCursor: Cursor position {x, y}
                - screen: TTY number
                - screenUpdateTime: Timestamp
                - text: Decoded text content
                - attributes: Color/formatting attributes
        """
        event_data = {
            "bytes": vcsaContent,
            "lines": int(vcsaContent[0]),
            "columns": int(vcsaContent[1]),
            "textCursor": {"x": int(vcsaContent[2]), "y": int(vcsaContent[3])},
            "screen": screen,
            "screenUpdateTime": time.time(),
            "text": "",
            "attributes": [],
        }
        try:
            event_data["text"], event_data["attributes"] = (
                self.auto_decode_vcsa(
                    vcsaContent[4:], event_data["lines"], event_data["columns"]
                )
            )
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "vcsaDriver create_screen_event_data: Error decoding VCSA content: "
                + str(e),
                debug.DebugLevel.ERROR,
            )
        # VCSU seems to give b'   ' instead of b'\x00\x00\x00' (tsp),
        # deactivated until its fixed
        if vcsu_content is not None:
            try:
                vcsu_content_as_text = vcsu_content.decode("UTF-32")
                event_data["text"] = screen_utils.insert_newlines(
                    vcsu_content_as_text, event_data["columns"]
                )
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    "vcsaDriver create_screen_event_data: Error decoding VCSU content: "
                    + str(e),
                    debug.DebugLevel.ERROR,
                )
        return event_data.copy()

    def update_char_map(self, screen):
        """Update character mapping for the specified screen.
        
        Reads the Unicode font mapping from the TTY to properly decode
        character data from VCSA. This handles special characters and
        Unicode properly.
        
        Args:
            screen (str): TTY number to update mapping for
            
        Updates:
            self.charmap: Dictionary mapping byte values to Unicode characters
            self.hichar: High character mask for extended characters
        """
        self.charmap = {}
        try:
            with open("/dev/tty" + screen, "rb") as tty:
                GIO_UNIMAP = 0x4B66
                VT_GETHIFONTMASK = 0x560D
                himask = array("H", (0,))
                ioctl(tty, VT_GETHIFONTMASK, himask)
                (self.hichar,) = unpack_from("@H", himask)
                sz = 512
                line = ""
                while True:
                    try:
                        unipairs = array("H", [0] * (2 * sz))
                        unimapdesc = array(
                            "B", pack("@HP", sz, unipairs.buffer_info()[0])
                        )
                        ioctl(tty.fileno(), GIO_UNIMAP, unimapdesc)
                        break
                    except Exception as e:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "VCSA:update_char_map:scaling up sz="
                            + str(sz)
                            + " "
                            + str(e),
                            debug.DebugLevel.WARNING,
                        )
                        sz *= 2
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                "VCSA:update_char_map:" + str(e), debug.DebugLevel.ERROR
            )
            return
        (ncodes,) = unpack_from("@H", unimapdesc)
        utable = unpack_from("@%dH" % (2 * ncodes), unipairs)
        for u, b in zip(utable[::2], utable[1::2]):
            if self.charmap.get(b) is None:
                self.charmap[b] = chr(u)

    def auto_decode_vcsa(self, allData, rows, cols):
        """Decode raw VCSA data into text and attributes.
        
        Processes the character and attribute data from VCSA devices,
        extracting both the text content and formatting information
        (colors, bold, blink, etc.).
        
        Args:
            allData (bytes): Raw character and attribute data from VCSA
            rows (int): Number of screen rows
            cols (int): Number of screen columns
            
        Returns:
            tuple: (text_content, attributes)
                - text_content (str): Decoded text with newlines
                - attributes (list): List of attribute arrays for each character
                
        Note:
            Each character in VCSA is stored as 2 bytes: character + attribute.
            Attributes encode foreground/background colors, bold, blink, etc.
        """
        all_text = ""
        all_attrib = []
        i = 0
        for y in range(rows):
            line_text = ""
            line_attrib = []
            blink = 0
            bold = 0
            ink = 7
            paper = 0
            for x in range(cols):
                data = allData[i : i + 2]
                i += 2
                if data == b" \x07":
                    # attr = 7
                    # ink = 7
                    # paper = 0
                    # ch = ' '
                    char_attrib = [
                        self.fgColorValues[7],  # fg
                        self.bgColorValues[0],  # bg
                        False,  # bold
                        False,  # italics
                        False,  # underscore
                        False,  # strikethrough
                        False,  # reverse
                        False,  # blink
                        "default",  # fontsize
                        "default",
                    ]  # fontfamily
                    line_attrib.append(char_attrib)
                    line_text += " "
                    continue
                ch = None
                try:
                    (sh,) = unpack("=H", data)
                    attr = (sh >> 8) & 0xFF
                    ch = sh & 0xFF
                    try:
                        if sh & self.hichar:
                            ch |= 0x100
                    except Exception as e:
                        self.env["runtime"]["DebugManager"].write_debug_out(
                            "vcsaDriver auto_decode_vcsa: Error processing character: "
                            + str(e),
                            debug.DebugLevel.ERROR,
                        )
                        ch = None
                    if self.hichar == 0x100:
                        attr >>= 1
                    ink = attr & 0x0F
                    paper = (attr >> 4) & 0x0F
                    if attr & 1:
                        blink = 1
                    # blink seems to be set always, ignore for now
                    blink = 0
                    bold = 0
                    if attr & 16:
                        bold = 1
                    # if (ink != 7) or (paper != 0):
                    #    print(ink,paper)
                except Exception as e:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        "vcsaDriver auto_decode_vcsa: Error processing attributes: "
                        + str(e),
                        debug.DebugLevel.ERROR,
                    )
                try:
                    line_text += self.charmap[ch]
                except KeyError:
                    line_text += "?"

                char_attrib = [
                    self.fgColorValues[ink],
                    self.bgColorValues[paper],
                    bold == 1,  # bold
                    False,  # italics
                    False,  # underscore
                    False,  # strikethrough
                    False,  # reverse
                    blink == 1,  # blink
                    "default",  # fontsize
                    "default",
                ]  # fontfamily
                line_attrib.append(char_attrib)
            all_text += line_text
            if y + 1 < rows:
                all_text += "\n"
            all_attrib.append(line_attrib)
        return str(all_text), all_attrib

    def get_screen_process_for_tty(self, tty_num):
        """Find the screen process associated with specific TTY"""
        try:
            result = subprocess.run([
                'ps', '-eo', 'pid,ppid,comm,tty,stat', '--no-headers'
            ], capture_output=True, text=True, timeout=2)
            
            for line in result.stdout.strip().split('\n'):
                if not line.strip():
                    continue
                parts = line.split()
                if len(parts) >= 4 and parts[2] == 'screen' and f'tty{tty_num}' in parts[3]:
                    return parts[0]  # Return PID of screen process
        except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
            pass
        return None

    def get_screen_session_process(self, screen_tty_pid):
        """Get the session manager process for a TTY screen process"""
        try:
            result = subprocess.run([
                'ps', '-eo', 'pid,ppid,comm', '--no-headers'
            ], capture_output=True, text=True, timeout=2)
            
            for line in result.stdout.strip().split('\n'):
                if not line.strip():
                    continue
                parts = line.split()
                if len(parts) >= 3 and parts[1] == screen_tty_pid and parts[2] == 'screen':
                    return parts[0]  # Return session manager PID
        except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
            pass
        return None

    def parse_active_app_from_pstree(self, pstree_output):
        """Parse pstree output to find currently active application"""
        try:
            # Look for processes that indicate active applications
            # Example: screen(1786)---bash(1787)---irssi(2016)
            import re
            
            # Find all application processes (non-bash, non-screen)
            # Pattern excludes --- prefix from pstree connection lines
            app_pattern = r'([a-zA-Z0-9_]+)\((\d+)\)'
            matches = re.findall(app_pattern, pstree_output)
            
            skip_processes = {'screen', 'bash', 'sh', 'grep', 'ps', 'sudo', 'sleep', 'clipboard_sync'}
            applications = []
            
            for app_name, pid in matches:
                if app_name.lower() not in skip_processes:
                    # Check if this process is in foreground state
                    try:
                        ps_result = subprocess.run([
                            'ps', '-p', pid, '-o', 'stat', '--no-headers'
                        ], capture_output=True, text=True, timeout=1)
                        
                        if ps_result.returncode == 0:
                            stat = ps_result.stdout.strip()
                            # Prioritize active processes
                            priority = 0
                            if 'S' in stat or 'R' in stat:  # Active processes
                                priority += 10
                            if '+' in stat:  # Foreground processes
                                priority += 20
                            if 'l' in stat.lower():  # Locked processes
                                priority += 5
                                
                            applications.append((app_name, pid, priority, stat))
                    except:
                        # If we can't check status, still consider it with low priority
                        applications.append((app_name, pid, 1, 'unknown'))
            
            # Sort by priority and return the highest priority application
            if applications:
                applications.sort(key=lambda x: (x[2], int(x[1])), reverse=True)
                best_app = applications[0][0].upper()
                
                self.env["runtime"]["DebugManager"].write_debug_out(
                    f"parse_active_app_from_pstree found {len(applications)} apps, selected: {best_app}",
                    debug.DebugLevel.INFO
                )
                return best_app
                
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                f"Error parsing pstree output: {str(e)}", debug.DebugLevel.ERROR
            )
        return None

    def get_app_from_screen_session(self, tty_num):
        """Get current application from active screen window on specific TTY"""
        try:
            # Find the screen session PID for this TTY
            ps_result = subprocess.run([
                'ps', '-eo', 'pid,ppid,comm,tty,args', '--no-headers'
            ], capture_output=True, text=True, timeout=2)
            
            if ps_result.returncode == 0:
                # Find the main screen process for our TTY
                screen_pid = None
                for line in ps_result.stdout.split('\n'):
                    if not line.strip():
                        continue
                    parts = line.split(None, 4)
                    if len(parts) >= 4 and parts[2] == 'screen' and f'tty{tty_num}' in parts[3]:
                        screen_pid = parts[0]
                        break
                
                if not screen_pid:
                    return None
                
                # Get the session manager screen process (child of TTY screen)
                session_pid = None
                for line in ps_result.stdout.split('\n'):
                    if not line.strip():
                        continue
                    parts = line.split(None, 4)
                    if len(parts) >= 3 and parts[1] == screen_pid and parts[2] == 'screen':
                        session_pid = parts[0]
                        break
                
                if not session_pid:
                    return None
                    
                self.env["runtime"]["DebugManager"].write_debug_out(
                    f"Found screen session PID: {session_pid}",
                    debug.DebugLevel.INFO
                )
                
                # Get all bash processes under this screen session
                bash_processes = []
                for line in ps_result.stdout.split('\n'):
                    if not line.strip():
                        continue
                    parts = line.split(None, 4)
                    if len(parts) >= 3 and parts[1] == session_pid and parts[2] == 'bash':
                        bash_processes.append(parts[0])
                
                # Check each bash for child applications, prioritizing active ones
                best_app = None
                best_priority = 0
                
                for bash_pid in bash_processes:
                    child_result = subprocess.run([
                        'ps', '--ppid', bash_pid, '-o', 'comm,stat', '--no-headers'
                    ], capture_output=True, text=True, timeout=1)
                    
                    if child_result.returncode == 0:
                        for child_line in child_result.stdout.split('\n'):
                            if not child_line.strip():
                                continue
                            child_parts = child_line.split()
                            if len(child_parts) >= 2:
                                child_comm, child_stat = child_parts[0], child_parts[1]
                                if child_comm.lower() not in ['grep', 'ps', 'bash', 'sh', 'sudo', 'sleep']:
                                    # Calculate priority based on process state
                                    priority = 0
                                    if 'R' in child_stat:  # Running
                                        priority += 30
                                    elif 'S' in child_stat:  # Sleeping
                                        priority += 20
                                    if '+' in child_stat:  # Foreground
                                        priority += 20
                                    
                                    self.env["runtime"]["DebugManager"].write_debug_out(
                                        f"Found child process: {child_comm} ({child_stat}) priority: {priority}",
                                        debug.DebugLevel.INFO
                                    )
                                    
                                    if priority > best_priority:
                                        best_app = child_comm.upper()
                                        best_priority = priority
                
                if best_app:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        f"Selected best app: {best_app} (priority: {best_priority})",
                        debug.DebugLevel.INFO
                    )
                    return best_app
                
        except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError) as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                f"Error getting app from screen session: {str(e)}", debug.DebugLevel.ERROR
            )
        return None

    def parse_latest_active_app_from_pstree(self, pstree_output):
        """Parse pstree output to find the most recently active application"""
        try:
            import re
            
            # Find all application processes with PIDs
            app_pattern = r'([a-zA-Z0-9_]+)\((\d+)\)'
            matches = re.findall(app_pattern, pstree_output)
            
            skip_processes = {'screen', 'bash', 'sh', 'grep', 'ps', 'sudo', 'sleep', 'clipboard_sync'}
            applications = []
            
            for app_name, pid in matches:
                if app_name.lower() not in skip_processes:
                    # Check if this process is still active and get its start time
                    try:
                        ps_result = subprocess.run([
                            'ps', '-p', pid, '-o', 'stat,lstart', '--no-headers'
                        ], capture_output=True, text=True, timeout=1)
                        
                        if ps_result.returncode == 0:
                            stat_info = ps_result.stdout.strip().split(None, 1)
                            if len(stat_info) >= 2:
                                stat = stat_info[0]
                                start_time = stat_info[1]
                                
                                # Prioritize processes that are active or have recent activity
                                priority = 0
                                if 'S' in stat or 'R' in stat:  # Running or sleeping (active)
                                    priority += 10
                                if '+' in stat:  # Foreground process
                                    priority += 20
                                    
                                applications.append((app_name, pid, priority, start_time))
                    except:
                        # If we can't get status, still consider it but with low priority
                        applications.append((app_name, pid, 1, 'unknown'))
            
            # Sort by priority (highest first), then by PID (most recent)
            if applications:
                applications.sort(key=lambda x: (x[2], int(x[1])), reverse=True)
                best_app = applications[0][0].upper()
                
                self.env["runtime"]["DebugManager"].write_debug_out(
                    f"Found {len(applications)} applications, selected: {best_app}",
                    debug.DebugLevel.INFO
                )
                return best_app
                
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                f"Error parsing latest active app from pstree: {str(e)}", 
                debug.DebugLevel.ERROR
            )
        return None

    def get_app_from_tmux_session(self, tty_num):
        """Get current application from tmux session on specific TTY"""
        try:
            # Try tmux list-panes to find active application
            result = subprocess.run([
                'tmux', 'list-panes', '-F', '#{pane_active} #{pane_current_command} #{pane_tty}'
            ], capture_output=True, text=True, timeout=2)
            
            if result.returncode == 0:
                for line in result.stdout.strip().split('\n'):
                    if not line.strip():
                        continue
                    parts = line.split()
                    if len(parts) >= 3 and parts[0] == '1':  # Active pane
                        tty_part = parts[2]
                        if tty_num in tty_part:
                            app = parts[1].upper()
                            if app not in ['BASH', 'SH']:
                                self.env["runtime"]["DebugManager"].write_debug_out(
                                    f"Found tmux application: {app}",
                                    debug.DebugLevel.INFO
                                )
                                return app
                    
        except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError):
            pass
        return None

    def get_curr_application(self):
        """Enhanced application detection supporting screen/tmux sessions.
        
        Multi-method approach:
        1. Try screen session detection via process tree analysis
        2. Try tmux session detection via tmux commands  
        3. Fall back to standard ps-based detection
        
        Updates:
            env['screen']['new_application']: Name of current application
            
        Features:
            - Detects applications inside screen/tmux sessions
            - Handles multiple screen sessions on different TTYs
            - Provides detailed debug logging for troubleshooting
        """
        curr_screen = self.env["screen"]["newTTY"]
        detected_app = None
        
        self.env["runtime"]["DebugManager"].write_debug_out(
            f"Starting application detection for TTY{curr_screen}",
            debug.DebugLevel.INFO
        )
        
        # Method 1: Try screen session detection
        try:
            detected_app = self.get_app_from_screen_session(curr_screen)
            if detected_app:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    f"Screen session detection found: {detected_app}",
                    debug.DebugLevel.INFO
                )
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                f"Screen session detection failed: {str(e)}", debug.DebugLevel.ERROR
            )
        
        # Method 2: Try tmux session detection
        if not detected_app:
            try:
                detected_app = self.get_app_from_tmux_session(curr_screen)
                if detected_app:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        f"Tmux session detection found: {detected_app}",
                        debug.DebugLevel.INFO
                    )
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    f"Tmux session detection failed: {str(e)}", debug.DebugLevel.ERROR
                )
        
        # Method 3: Fall back to standard ps-based detection
        if not detected_app:
            try:
                detected_app = self.get_app_via_standard_ps(curr_screen)
                if detected_app:
                    self.env["runtime"]["DebugManager"].write_debug_out(
                        f"Standard ps detection found: {detected_app}",
                        debug.DebugLevel.INFO
                    )
            except Exception as e:
                self.env["runtime"]["DebugManager"].write_debug_out(
                    f"Standard ps detection failed: {str(e)}", debug.DebugLevel.ERROR
                )
        
        # Update application if we found one and it's different
        if detected_app and self.env["screen"]["new_application"] != detected_app:
            self.env["screen"]["new_application"] = detected_app
            self.env["runtime"]["DebugManager"].write_debug_out(
                f"Application changed to: {detected_app}",
                debug.DebugLevel.INFO
            )

    def get_app_via_standard_ps(self, curr_screen):
        """Fallback ps-based application detection for non-screen/tmux environments"""
        try:
            # Simple TTY-specific detection as fallback
            result = subprocess.run([
                'ps', '-t', f'tty{curr_screen}', '-o', 'comm,stat', '--no-headers'
            ], capture_output=True, text=True, timeout=2)
            
            if result.returncode == 0:
                for line in result.stdout.split('\n'):
                    if not line.strip():
                        continue
                    parts = line.split()
                    if len(parts) >= 2:
                        comm, stat = parts[0], parts[1]
                        if '+' in stat and comm.upper() not in ['BASH', 'SH', 'SCREEN', 'GREP', 'PS']:
                            self.env["runtime"]["DebugManager"].write_debug_out(
                                f"Fallback detection found: {comm}",
                                debug.DebugLevel.INFO
                            )
                            return comm.upper()
        except Exception as e:
            self.env["runtime"]["DebugManager"].write_debug_out(
                f"Standard ps detection error: {str(e)}", debug.DebugLevel.ERROR
            )
        return None
