Module: Muxr::ForegroundCommand

Defined in:
lib/muxr/foreground_command.rb

Overview

Looks up the foreground command running inside a PTY by walking from the shell’s pid → its tpgid (foreground process group on the controlling tty) → that process’s command name. Hides the result when the shell itself is foreground so titles aren’t full of “bash” / “zsh” noise.

Two platform paths:

Linux: /proc/<pid>/stat (no fork — runs fast even on the main thread,
       though Application uses a background thread anyway)
macOS: ps -o tpgid=,pgid= -p <pid> + ps -o comm= -p <tpgid>. Two
       fork+execs, ~10–20ms total — exactly the reason callers run
       this off the event-loop thread.

Returns the command name string or nil. nil also covers “couldn’t read” so callers degrade silently rather than risk showing stale data.

Constant Summary collapse

SHELLS =

Command names we never want to surface — these are the empty-prompt case. If a user genuinely runs ‘bash` inside `bash` we’ll under-report rather than mis-report.

%w[bash zsh fish sh dash ksh tcsh csh].freeze

Class Method Summary collapse

Class Method Details

.linux_tpgid(pid) ⇒ Object



74
75
76
77
# File 'lib/muxr/foreground_command.rb', line 74

def linux_tpgid(pid)
  raw = File.read("/proc/#{pid}/stat")
  parse_linux_stat(raw)
end

.lookup(pid) ⇒ Object



24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# File 'lib/muxr/foreground_command.rb', line 24

def lookup(pid)
  return nil unless pid.is_a?(Integer) && pid > 0
  tpgid, pgid =
    if File.exist?("/proc/#{pid}/stat")
      linux_tpgid(pid)
    else
      macos_tpgid(pid)
    end
  return nil unless tpgid && pgid
  return nil if tpgid <= 0
  return nil if tpgid == pgid # shell is its own foreground — empty prompt

  name =
    if File.exist?("/proc/#{tpgid}/comm")
      File.read("/proc/#{tpgid}/comm").strip
    else
      `ps -o comm= -p #{tpgid} 2>/dev/null`.strip
    end
  normalize(name)
rescue StandardError
  nil
end

.macos_tpgid(pid) ⇒ Object



79
80
81
82
83
84
# File 'lib/muxr/foreground_command.rb', line 79

def macos_tpgid(pid)
  out = `ps -o tpgid=,pgid= -p #{pid} 2>/dev/null`.strip
  return [nil, nil] if out.empty?
  tpgid, pgid = out.split.map(&:to_i)
  [tpgid, pgid]
end

.normalize(name) ⇒ Object

Public for testing — strips path/dash/whitespace and filters shells.



48
49
50
51
52
53
54
55
56
# File 'lib/muxr/foreground_command.rb', line 48

def normalize(name)
  return nil if name.nil? || name.empty?
  name = name.strip
  name = name.sub(/\A-/, "") # login shells appear as "-bash"
  name = File.basename(name)
  return nil if name.empty?
  return nil if SHELLS.include?(name)
  name
end

.parse_linux_stat(raw) ⇒ Object

Public for testing — parses Linux /proc/<pid>/stat into [tpgid, pgid]. The comm field can contain spaces and parens, so we slice from the last ‘)’ rather than splitting from the start.



61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/muxr/foreground_command.rb', line 61

def parse_linux_stat(raw)
  idx = raw.rindex(")")
  return [nil, nil] unless idx
  tail = raw[(idx + 2)..]
  return [nil, nil] unless tail
  fields = tail.split(" ")
  # After the closing paren the fields are:
  #   state(0) ppid(1) pgrp(2) session(3) tty_nr(4) tpgid(5) ...
  pgid  = fields[2]&.to_i
  tpgid = fields[5]&.to_i
  [tpgid, pgid]
end