Class: Echoes::EmbeddedShellHelper

Inherits:
Object
  • Object
show all
Defined in:
lib/echoes/embedded_shell_helper.rb

Constant Summary collapse

DARWIN_TIOCSCTTY =
0x20007461

Instance Method Summary collapse

Constructor Details

#initializeEmbeddedShellHelper

Returns a new instance of EmbeddedShellHelper.



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/echoes/embedded_shell_helper.rb', line 38

def initialize
  Process.setsid rescue nil
  STDIN.ioctl(DARWIN_TIOCSCTTY, 0) rescue nil
  # After claiming ctty, explicitly set the slave's foreground
  # process group to ours. Without this, the line discipline has
  # nowhere to deliver SIGINT (the kernel doesn't do it automatically
  # on macOS via TIOCSCTTY) and Ctrl-C is silently lost.
  HelperLibc.tcsetpgrp(0, Process.getpgrp) rescue nil
  # The helper and any rubish-forked child share the helper's
  # process group (= slave's foreground pgrp once we claim ctty).
  # ETX → SIGINT is delivered to that whole group. We want:
  #   - the forked external command to terminate (its default
  #     handler does this after exec)
  #   - the helper *process* to survive (so the pane keeps running)
  #   - the rubish *command thread* to abort (so a for-loop's body
  #     stops iterating instead of just dropping the current sleep
  #     and starting the next one).
  # A Ruby trap-with-block survives across exec as SIG_DFL (kernel
  # only preserves SIG_IGN), so the exec'd binary still gets the
  # default-terminate behavior. The block runs on the helper main
  # thread; from there we raise Interrupt on the command thread.
  Signal.trap('INT')  { @command_thread&.raise(Interrupt) rescue nil }
  Signal.trap('QUIT') { @command_thread&.raise(Interrupt) rescue nil }
  no_rc = ENV['ECHOES_HELPER_NO_RC'] == '1'
  # login_shell: true so rubish sources /etc/profile (which runs
  # path_helper, populating PATH from /etc/paths and /etc/paths.d
  # — including /usr/local/bin and the macOS cryptex paths). The
  # embedded rubish IS the only shell in the pane, so treating it
  # as a login shell is correct, and matches how Ghostty (and any
  # other terminal) launches the user's $SHELL.
  @repl = Rubish::REPL.new(no_rc: no_rc, login_shell: true)
  # Rubish normally calls these from its `run` loop, which we
  # bypass — the line editor and prompt rendering live in echoes.
  # Drive them explicitly so ~/.rubishrc et al take effect,
  # default aliases land in the env, and history is restored.
  # Errors land on stderr (= the pty, visible in the pane) so
  # silent failures during startup don't disappear into the void.
  run_init_step(:setup_default_aliases)
  run_init_step(:load_config)
  run_init_step(:load_history) unless no_rc
  @control_in  = IO.for_fd(3, 'r')
  @control_out = IO.for_fd(4, 'w')
  @control_out.sync = true
  @write_lock = Mutex.new
  @command_thread = nil
  # OSC 7 announces the working directory to the host so things
  # like new-tab-inherits-cwd and the window title pick it up via
  # the standard `screen.current_directory` path. Emit once at
  # startup; thereafter handle_execute re-emits after each command
  # in case it changed cwd (cd, pushd/popd, …).
  emit_osc7(Dir.pwd)
end

Instance Method Details

#runObject



91
92
93
94
95
96
97
# File 'lib/echoes/embedded_shell_helper.rb', line 91

def run
  while (line = @control_in.gets)
    msg = (JSON.parse(line) rescue nil)
    next unless msg
    dispatch(msg)
  end
end