Example: Plaintext

For example, let’s manually install Plaintext, rather than relying on Outer Shell to do it for us.

Plaintext is a more complete example:

  • It listens on a Unix socket, not a TCP port.
  • The service manager owns the socket and starts the backend on first input.
  • It registers itself as an opener for public.text.

The paths below assume you already have the Plaintext backend executable and content bundles installed somewhere. Adjust them to match your install location.

Linux

Create ~/.config/systemd/user/org.outershell.Plaintext.service:

[Unit]
Description=Plaintext
After=network.target

[Service]
Type=simple
WorkingDirectory=%h/.local/share/outershell/apps/org.outershell.Plaintext
Environment=OUTERSHELLD_API_SOCKET=%t/outershelld-api
ExecStart=%h/.local/share/outershell/apps/org.outershell.Plaintext/PlaintextBackend \
  --label org.outershell.Plaintext \
  --socket-path %t/org.outershell.Plaintext \
  --bundles-dir %h/.local/share/outershell/apps/org.outershell.Plaintext/bundles
Restart=on-failure

[Install]
WantedBy=default.target

Create ~/.config/systemd/user/org.outershell.Plaintext.socket:

[Unit]
Description=Plaintext Socket

[Socket]
ListenStream=%t/org.outershell.Plaintext
FileDescriptorName=http
SocketMode=0600

[Install]
WantedBy=sockets.target

Enable the socket:

systemctl --user daemon-reload
systemctl --user enable --now org.outershell.Plaintext.socket

Register the backend and app endpoint:

SOCKET_PATH="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/org.outershell.Plaintext"

outerctl backend upsert \
  --backend org.outershell.Plaintext \
  --name Plaintext \
  --systemd-unit org.outershell.Plaintext.service

outerctl app upsert \
  --backend org.outershell.Plaintext \
  --name Plaintext \
  --socket-path "$SOCKET_PATH" \
  --path /

Register the file association:

outerctl opener upsert \
  --backend org.outershell.Plaintext \
  --content-type public.text \
  --url-template "?file={file}" \
  --rank 0 \
  --capabilities view,edit

public.text is a built-in content type, so you do not need to create it with outerctl content-type add.

macOS

On macOS, launchd owns the socket via the plist’s Sockets dictionary. The backend calls launch_activate_socket("Listener", ...) to receive the listener that launchd created.

First find the per-user temporary directory. This is where Outer Shell expects user-level Unix sockets:

SOCKET_PATH="$(getconf DARWIN_USER_TEMP_DIR)org.outershell.Plaintext"

Create ~/Library/LaunchAgents/org.outershell.Plaintext.plist.

Replace /Users/example/Library/Application Support/outershell/apps/org.outershell.Plaintext with your install location:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>org.outershell.Plaintext</string>

  <key>ProgramArguments</key>
  <array>
    <string>/Users/example/Library/Application Support/outershell/apps/org.outershell.Plaintext/PlaintextBackend</string>
    <string>--label</string>
    <string>org.outershell.Plaintext</string>
    <string>--socket-path</string>
    <string>/var/folders/.../T/org.outershell.Plaintext</string>
    <string>--launchd-socket-name</string>
    <string>Listener</string>
    <string>--bundles-dir</string>
    <string>/Users/example/Library/Application Support/outershell/apps/org.outershell.Plaintext/bundles</string>
  </array>

  <key>EnvironmentVariables</key>
  <dict>
    <key>OUTERSHELLD_API_SOCKET</key>
    <string>/var/folders/.../T/outershelld-api</string>
  </dict>

  <key>WorkingDirectory</key>
  <string>/Users/example/Library/Application Support/outershell/apps/org.outershell.Plaintext</string>

  <key>StandardOutPath</key>
  <string>/Users/example/Library/Application Support/outershell/apps/org.outershell.Plaintext/backend.log</string>
  <key>StandardErrorPath</key>
  <string>/Users/example/Library/Application Support/outershell/apps/org.outershell.Plaintext/backend.log</string>

  <key>Sockets</key>
  <dict>
    <key>Listener</key>
    <dict>
      <key>SockPathName</key>
      <string>/var/folders/.../T/org.outershell.Plaintext</string>
      <key>SockPathMode</key>
      <integer>600</integer>
    </dict>
  </dict>
</dict>
</plist>

The /var/folders/.../T/ strings should be the value from getconf DARWIN_USER_TEMP_DIR. The socket path in ProgramArguments and Sockets must match.

Bootstrap the plist:

launchctl bootstrap "gui/$(id -u)" "$HOME/Library/LaunchAgents/org.outershell.Plaintext.plist"

Register the backend and app endpoint:

outerctl backend upsert \
  --backend org.outershell.Plaintext \
  --name Plaintext \
  --launchd-plist "$HOME/Library/LaunchAgents/org.outershell.Plaintext.plist"

outerctl app upsert \
  --backend org.outershell.Plaintext \
  --name Plaintext \
  --socket-path "$SOCKET_PATH" \
  --path /

Register the file association:

outerctl opener upsert \
  --backend org.outershell.Plaintext \
  --content-type public.text \
  --url-template "?file={file}" \
  --rank 0 \
  --capabilities view,edit

Again, public.text is built in. The opener registration is the part that tells Outer Shell that Plaintext can appear as an app for text files.