Skip to content

Vaults Log should output to $PAGER when available #379

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
CMCDragonkai opened this issue Mar 24, 2025 · 6 comments
Open

Vaults Log should output to $PAGER when available #379

CMCDragonkai opened this issue Mar 24, 2025 · 6 comments
Labels
development Standard development

Comments

@CMCDragonkai
Copy link
Member

CMCDragonkai commented Mar 24, 2025

Specification

When executing the polykey vaults log command, the CLI should automatically detect and utilize the system's PAGER (e.g., less, more) if it exists. If no PAGER is available, the command should default to streaming the output directly to the terminal. This behavior should be consistent across different operating systems, including Windows and MacOS. The implementation should be inspired by git log, which similarly uses a PAGER for its output.

Additional context

  • Prior Work: The git log command is a well-known example of this behavior, and its implementation can serve as a reference. It defaults to less as the PAGER on Unix-like systems and uses a pager-like mechanism on Windows.
  • Cross-Platform Considerations:
    • On Unix-like systems (e.g., MacOS, Linux), the PAGER is typically set in the environment variables (e.g., $PAGER).
    • On Windows, common pagers like more are available, and the behavior should mimic Unix systems as closely as possible.
    • If no PAGER is detected, the output should stream directly to the terminal, ensuring the command remains functional even in environments without a PAGER.
  • https://unix.stackexchange.com/a/213369/56970

Tasks

  1. Pager Detection: Implement logic to detect the presence of a PAGER in the system environment variables. On Unix-like systems, check for $PAGER. On Windows, check for the availability of more or other pagers.
  2. Fallback Mechanism: If no PAGER is detected, ensure the command streams the output directly to the terminal.
  3. Cross-Platform Integration: Ensure the implementation works consistently across Unix-like systems and Windows, handling the differences in how pagers are managed on each platform.
  4. Testing: Implement unit tests and integration tests to verify the behavior in different environments (with/without PAGER, on Windows/MacOS/Linux).
  5. Documentation: Update the CLI documentation to reflect this behavior, including examples of how to set a custom PAGER if desired.
@CMCDragonkai CMCDragonkai added the development Standard development label Mar 24, 2025
Copy link

linear bot commented Mar 24, 2025

ENG-564

@CMCDragonkai
Copy link
Member Author

After investigating how best to implement PAGER support across platforms, I recommend the following approach:

/**
 * Gets the appropriate pager based on environment variables
 */
function getPager(): string | null {
  // Check for PAGER environment variable (standard across all platforms)
  if (process.env.PAGER) {
    return process.env.PAGER;
  }
  
  // On Windows, 'more' is a built-in command available in both CMD and PowerShell
  if (process.platform === 'win32') {
    return 'more';
  }
  
  // For Unix-like systems (Linux, macOS), don't assume any default
  return null;
}

@CMCDragonkai
Copy link
Member Author

This should only be used when the STDOUT is a real terminal. Don't use when the STDOUT is pointing to a pipe.


When git log encounters an invalid PAGER or a pager failure, it handles the situation gracefully by:

  1. Falling Back to stdout: If the specified PAGER fails to execute or is invalid, git log switches to directly outputting to stdout (the terminal). This ensures the command remains functional even if the pager fails.

  2. Error Output: If the PAGER is explicitly set (e.g., via PAGER environment variable or Git's core.pager configuration), but it is invalid or fails to execute, Git will output an error message to stderr. For example:

    error: pager failed: <pager command>
    
  3. No Logging: Git does not log pager failures to files or persist them. The error message is transient and only displayed in the terminal session.

Behavior in Specific Scenarios

  • Invalid PAGER: If PAGER is set to a command that does not exist (e.g., PAGER=non-existent-command), Git outputs an error to stderr and falls back to stdout.
  • Pager Crash: If the pager process crashes or exits with a non-zero status, Git detects this and reverts to stdout output.
  • No PAGER: If PAGER is not set, Git defaults to less on Unix-like systems or more on Windows, and if neither is available, it uses stdout.

Implementation Details in Git

Git's pager handling is implemented in its pager.c module. Key behaviors include:

  • Detecting and executing the pager via fork() and execvp() (Unix) or CreateProcess() (Windows).
  • Monitoring the pager process for failure or abnormal termination.
  • Redirecting output to stdout if pager execution fails.

Summary

Git prioritizes robustness by ensuring git log always produces output, even if the pager fails. It combines a transparent fallback to stdout with explicit error messaging to stderr for debugging. This approach balances usability and debugging capability without compromising functionality.

@CMCDragonkai
Copy link
Member Author

Sample code summarisation:

import { spawn } from 'child_process';

/**
 * Gets the appropriate pager based on environment variables
 * @returns The pager command or null if none is available
 */
function getPager(): string | null {
  // Check for PAGER environment variable (standard across all platforms)
  if (process.env.PAGER) {
    return process.env.PAGER;
  }
  
  // On Windows, 'more' is a built-in command available in both CMD and PowerShell
  if (process.platform === 'win32') {
    return 'more';
  }
  
  // For Unix-like systems (Linux, macOS), don't assume any default
  return null;
}

/**
 * Determines if stdout is an interactive terminal
 */
function isStdoutInteractive(): boolean {
  return process.stdout.isTTY;
}

/**
 * Displays content using the system's pager or falls back to direct output
 * @param content Content to display
 */
async function displayWithPager(content: string): Promise<void> {
  // Only use a pager if stdout is an interactive terminal
  if (!isStdoutInteractive()) {
    console.log(content);
    return;
  }
  
  const pager = getPager();
  
  if (!pager) {
    // No pager available, use direct console output
    console.log(content);
    return;
  }
  
  try {
    // Split the pager command to handle arguments properly
    const [command, ...args] = pager.split(' ');
    
    const pagerProcess = spawn(command, args, {
      stdio: ['pipe', 'inherit', 'inherit'],
      shell: true
    });
    
    pagerProcess.stdin.write(content);
    pagerProcess.stdin.end();
    
    // Handle process completion
    return new Promise((resolve) => {
      pagerProcess.on('close', (code) => {
        if (code !== 0) {
          // Fall back to direct output if pager fails
          console.log(content);
        }
        resolve();
      });
      
      pagerProcess.on('error', (err) => {
        console.error(`Pager error: ${err.message}`);
        console.log(content);
        resolve();
      });
    });
  } catch (error) {
    // Fallback to direct output if pager fails to launch
    console.error(`Pager failed: ${error.message}`);
    console.log(content);
  }
}

// Helper to generate sample content
function generateSampleLogContent(): string {
  const lines = [];
  for (let i = 0; i < 100; i++) {
    lines.push(`[${new Date().toISOString()}] Log entry ${i}: Sample vault activity`);
  }
  return lines.join('\n');
}

// Example usage
async function main() {
  const logContent = generateSampleLogContent();
  await displayWithPager(logContent);
}

main().catch(console.error);

Will require synthesis into codebase though and I imagine the output handling would be augmented with PAGER if the output format is like a list of items. It would work more similarly to other systemd commands too. This also means piping to stuff works fine too and does involve the pager.

@CMCDragonkai
Copy link
Member Author

TTY detection enables subsequent colourisation and font formatting, which represents CLI UI/UX separately. But must be carefully done to preserve machine interpretation - even when just copy pasting from the terminal output.

@CMCDragonkai
Copy link
Member Author

https://dandavison.github.io/delta/environment-variables.html#pager-environment-variables - can also backup to less -R when PAGER not set, but has to be careful to fallback when command doesn't exist. Which would require a very quick command detection in the $PATH.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
development Standard development
Development

No branches or pull requests

1 participant