From c7cc9d039bc19fc6b66b69b2fafcb7ff944e117b Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Mon, 7 Jul 2025 11:17:12 -0400 Subject: [PATCH] Improved application detection, now works inside screen and tmux. Fixed incosistancies in prev/next word navigation. --- .../commands/commands/review_next_word.py | 10 +- .../commands/commands/review_prev_word.py | 4 + src/fenrirscreenreader/core/screenManager.py | 10 + .../screenDriver/vcsaDriver.py | 255 +++++++++++++++--- 4 files changed, 238 insertions(+), 41 deletions(-) diff --git a/src/fenrirscreenreader/commands/commands/review_next_word.py b/src/fenrirscreenreader/commands/commands/review_next_word.py index e101869c..55343954 100644 --- a/src/fenrirscreenreader/commands/commands/review_next_word.py +++ b/src/fenrirscreenreader/commands/commands/review_next_word.py @@ -57,13 +57,9 @@ class command: return # ALWAYS return in table mode to prevent regular word navigation # Regular word navigation (only when NOT in table mode) - self.env["screen"]["oldCursorReview"] = self.env["screen"][ - "newCursorReview" - ] - if self.env["screen"]["newCursorReview"] is None: - self.env["screen"]["newCursorReview"] = self.env["screen"][ - "new_cursor" - ].copy() + self.env["runtime"][ + "CursorManager" + ].enter_review_mode_curr_text_cursor() ( self.env["screen"]["newCursorReview"]["x"], diff --git a/src/fenrirscreenreader/commands/commands/review_prev_word.py b/src/fenrirscreenreader/commands/commands/review_prev_word.py index a2a56fe6..b9a2af31 100644 --- a/src/fenrirscreenreader/commands/commands/review_prev_word.py +++ b/src/fenrirscreenreader/commands/commands/review_prev_word.py @@ -39,6 +39,10 @@ class command: self.env["runtime"]["OutputManager"].present_text( output_text, interrupt=True, flush=False ) + # Play start of line sound + self.env["runtime"]["OutputManager"].present_text( + _("start of line"), interrupt=False, sound_icon="StartOfLine" + ) elif table_info: # Normal column navigation - announce cell content with column info output_text = f"{table_info['cell_content']} {table_info['column_header']}" diff --git a/src/fenrirscreenreader/core/screenManager.py b/src/fenrirscreenreader/core/screenManager.py index 54a2618e..2e646cb9 100644 --- a/src/fenrirscreenreader/core/screenManager.py +++ b/src/fenrirscreenreader/core/screenManager.py @@ -100,6 +100,15 @@ class ScreenManager: if self.is_curr_screen_ignored_changed(): self.env["runtime"]["InputManager"].set_execute_device_grab() self.env["runtime"]["InputManager"].handle_device_grab() + + # Update current application detection on screen change + try: + self.env["runtime"]["ScreenDriver"].get_curr_application() + except Exception as e: + self.env["runtime"]["DebugManager"].write_debug_out( + f"Application detection failed: {str(e)}", debug.DebugLevel.ERROR + ) + if not self.is_ignored_screen(self.env["screen"]["newTTY"]): self.update(event_data, "onScreenChange") self.env["screen"]["lastScreenUpdate"] = time.time() @@ -163,6 +172,7 @@ class ScreenManager: self.env["screen"]["newTTY"] = event_data["screen"] self.env["screen"]["new_content_text"] = event_data["text"] + # screen change if self.is_screen_change(): self.env["screen"]["oldContentBytes"] = b"" diff --git a/src/fenrirscreenreader/screenDriver/vcsaDriver.py b/src/fenrirscreenreader/screenDriver/vcsaDriver.py index 3705306e..bdc95684 100644 --- a/src/fenrirscreenreader/screenDriver/vcsaDriver.py +++ b/src/fenrirscreenreader/screenDriver/vcsaDriver.py @@ -729,23 +729,226 @@ class driver(screenDriver): 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) + app_pattern = r'([a-zA-Z0-9_-]+)\((\d+)\)' + matches = re.findall(app_pattern, pstree_output) + + skip_processes = {'screen', 'bash', 'sh', 'grep', 'ps'} + 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() + # Look for processes that are active (S+ state or similar) + if '+' in stat or 'l' in stat.lower(): + applications.append((app_name, pid, stat)) + except: + # If we can't check status, still consider it + applications.append((app_name, pid, 'unknown')) + + # Return the first active application found + if applications: + return applications[0][0].upper() + + 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 screen session on specific TTY""" + try: + # Step 1: Find screen process for this TTY + screen_tty_pid = self.get_screen_process_for_tty(tty_num) + if not screen_tty_pid: + return None + + self.env["runtime"]["DebugManager"].write_debug_out( + f"Found screen TTY process: {screen_tty_pid} for TTY{tty_num}", + debug.DebugLevel.INFO + ) + + # Step 2: Find session manager process + session_pid = self.get_screen_session_process(screen_tty_pid) + if not session_pid: + return None + + self.env["runtime"]["DebugManager"].write_debug_out( + f"Found screen session process: {session_pid}", + debug.DebugLevel.INFO + ) + + # Step 3: Get process tree and find active app + result = subprocess.run([ + 'pstree', '-p', session_pid + ], capture_output=True, text=True, timeout=3) + + if result.returncode == 0: + self.env["runtime"]["DebugManager"].write_debug_out( + f"Pstree output: {result.stdout[:200]}...", + debug.DebugLevel.INFO + ) + return self.parse_active_app_from_pstree(result.stdout) + + 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 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): - """Detect the currently running application on the active TTY. + """Enhanced application detection supporting screen/tmux sessions. - Uses 'ps' command to identify which process is currently in the - foreground on the active TTY, enabling application-specific features - like bookmarks and settings. + 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 - Note: - Filters out common shell processes (grep, sh, ps) to find the - actual user application. + Features: + - Detects applications inside screen/tmux sessions + - Handles multiple screen sessions on different TTYs + - Provides detailed debug logging for troubleshooting """ - apps = [] + 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): + """Original ps-based application detection as fallback""" try: - curr_screen = self.env["screen"]["newTTY"] apps = ( subprocess.Popen( "ps -t tty" + curr_screen + " -o comm,tty,stat", @@ -756,34 +959,18 @@ class driver(screenDriver): .decode()[:-1] .split("\n") ) - except Exception as e: - self.env["runtime"]["DebugManager"].write_debug_out( - str(e), debug.DebugLevel.ERROR - ) - return - try: + for i in apps: i = i.upper() i = i.split() - i[0] = i[0] - i[1] = i[1] - if "+" in i[2]: - if i[0] != "": - if ( - not "GREP" == i[0] - and not "SH" == i[0] - and not "PS" == i[0] - ): - if "TTY" + curr_screen in i[1]: - if ( - self.env["screen"]["new_application"] - != i[0] - ): - self.env["screen"]["new_application"] = i[ - 0 - ] - return + if len(i) >= 3: + comm, tty, stat = i[0], i[1], i[2] + if "+" in stat and comm != "": + if comm not in ["GREP", "SH", "PS", "BASH"]: + if "TTY" + curr_screen in tty: + return comm except Exception as e: self.env["runtime"]["DebugManager"].write_debug_out( - str(e), debug.DebugLevel.ERROR + f"Standard ps detection error: {str(e)}", debug.DebugLevel.ERROR ) + return None