Skip to content

Commit

Permalink
Merge pull request #7 from bash/better-detection
Browse files Browse the repository at this point in the history
Better detection, document caveats
  • Loading branch information
bash authored Jan 23, 2024
2 parents 87437ef + 751f8e2 commit a08850d
Show file tree
Hide file tree
Showing 15 changed files with 304 additions and 167 deletions.
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ license = "MIT OR Apache-2.0"
version = "0.1.0"
edition = "2021"
rust-version = "1.70.0"
exclude = [".github", ".gitignore"]
exclude = [".github", ".gitignore", "*.sh"]

[dependencies]
thiserror = "1.0.56"
rgb = { version = "0.8.37", optional = true }

[target.'cfg(unix)'.dependencies]
libc = "0.2.151"
memchr = "2.7.1"
mio = { version = "0.8.10", features = ["os-poll", "os-ext"], default-features = false }
terminal-trx = "0.1.0"

Expand Down
7 changes: 7 additions & 0 deletions doc/caveats.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Caveats

Extra care needs to be taken on Unix if your program might share
the terminal with another program. This might be the case
if you expect your output to be used with a pager e.g. `your_program` | `less`.
In that case, a race condition exists because the pager will also set the terminal to raw mode.
The `pager` example shows a heuristic to deal with this issue.
9 changes: 9 additions & 0 deletions docs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env bash

set -e

metadata=$(cargo metadata --format-version 1 --no-deps | jq '.packages | map(select(.name == "terminal-colorsaurus")) | first | .metadata.docs.rs')
features=$(echo "$metadata" | jq -r '.features | join(",")')

export RUSTDOCARGS=$(echo "$metadata" | jq -r '.["rustdoc-args"] | join(" ")')
cargo +nightly doc -Zunstable-options -Zrustdoc-scrape-examples --features "$features"
2 changes: 1 addition & 1 deletion examples/bg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::error::Error;
use terminal_colorsaurus::{background_color, QueryOptions};

fn main() -> Result<(), Box<dyn Error>> {
let fg = background_color(QueryOptions::default()).unwrap();
let fg = background_color(QueryOptions::default())?;
println!("rgb({}, {}, {})", fg.r >> 8, fg.g >> 8, fg.b >> 8);
Ok(())
}
2 changes: 1 addition & 1 deletion examples/fg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::error::Error;
use terminal_colorsaurus::{foreground_color, QueryOptions};

fn main() -> Result<(), Box<dyn Error>> {
let fg = foreground_color(QueryOptions::default()).unwrap();
let fg = foreground_color(QueryOptions::default())?;
println!("rgb({}, {}, {})", fg.r >> 8, fg.g >> 8, fg.b >> 8);
Ok(())
}
62 changes: 62 additions & 0 deletions examples/pager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use std::error::Error;
use terminal_colorsaurus::{color_scheme, QueryOptions};

/// This example is best in a couple of different ways:
/// 1. `cargo run --example pager`—should print the color scheme.
/// 2. `cargo run --example pager | less`—should not print the color scheme.
/// 3. `cargo run --example pager > file.txt`—should print the color scheme.
/// 4. `cargo run --example pager > /dev/null`—should print the color scheme.
fn main() -> Result<(), Box<dyn Error>> {
if should_auto_detect() {
eprintln!(
"Here's the color scheme: {:#?}",
color_scheme(QueryOptions::default())?
);
} else {
eprintln!("You're likely using a pager, doing nothing");
}
Ok(())
}

// Our stdout might be piped to a pager (e.g. `less`),
// in which case we have a race condition for enabling/disabling the raw mode
// and for reading/writing to the terminal.
//
// Note that this is just heuristic with both
// false negatives (output not piped to a pager) and
// false positives (stderr piped to a pager).
#[cfg(unix)]
fn should_auto_detect() -> bool {
use std::os::fd::AsFd;
!is_pipe(std::io::stdout().as_fd()).unwrap_or_default()
}

#[cfg(not(unix))]
fn should_auto_detect() -> bool {
true
}

// The mode can be bitwise AND-ed with S_IFMT to extract the file type code, and compared to the appropriate constant
// Source: https://www.gnu.org/software/libc/manual/html_node/Testing-File-Type.html
#[cfg(unix)]
fn is_pipe(fd: std::os::fd::BorrowedFd) -> std::io::Result<bool> {
use libc::{S_IFIFO, S_IFMT};
Ok(fstat(fd)?.st_mode & S_IFMT == S_IFIFO)
}

#[cfg(unix)]
fn fstat(fd: std::os::fd::BorrowedFd) -> std::io::Result<libc::stat> {
use std::os::fd::AsRawFd as _;
// SAFETY:
// 1. File descriptor is valid (we have a borrowed fd for the lifetime of this function)
// 2. fstat64 fills the stat structure for us (if successful).
unsafe {
let mut stat = std::mem::zeroed();
let ret = libc::fstat(fd.as_raw_fd(), &mut stat);
if ret == 0 {
Ok(stat)
} else {
Err(std::io::Error::last_os_error())
}
}
}
3 changes: 0 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ These are some features that I would like to include in this crate,
but have not yet had the time to implement. PRs are welcome :)

* [ ] A CLI tool version of this library.
* [ ] Improve dynamic latency strategy (iTerm is a bit of a headache here as it has quite a big range when it comes to latency).
* [ ] Add support for `$COLORFGBG`
(I don't know if this is a good idea actually, the [readme](https://github.com/Canop/terminal-light#colorfgbg-strategy) of `terminal-light` lists some downsides.)

## Inspiration
This crate borrows ideas from many other projects. This list is by no means exhaustive.
Expand Down
26 changes: 7 additions & 19 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,6 @@
//! ## Optional Dependencies
//! * [`rgb`] — Enable this feature to convert between [`Color`] and [`rgb::RGB16`].
//!
//! ## Variable Timeout
//! Knowing whether or not a terminal supports querying for the
//! foreground and background colors hard to reliably detect.
//! Employing a fixed timeout is not the best options because the terminal might support the sequence
//! but have a lot of latency (e.g. the user is connected over SSH).
//!
//! This library assumes that the terminal support the [widely supported][`terminal_survey`] `CSI c` sequence.
//! Using this, it measures the latency. This measurement then informs the timeout enforced on the actual query.
//!
//! ## Comparison with Other Crates
//! ### [termbg]
//! * Is hardcoded to use stdin/stderr for communicating with the terminal. \
Expand All @@ -93,8 +84,6 @@ use thiserror::Error;
mod color;
mod os;

#[cfg(unix)]
mod terminal;
#[cfg(windows)]
mod winapi;
#[cfg(unix)]
Expand Down Expand Up @@ -173,38 +162,37 @@ pub enum Error {
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct QueryOptions {
/// The maximum time spent waiting for a response from the terminal \
/// even when we *know* that the terminal supports querying for colors. Defaults to 1 s.
///
/// Note that this timeout might not always apply as we use a variable timeout
/// for the color query.
/// The maximum time spent waiting for a response from the terminal. Defaults to 1 s.
///
/// Consider leaving this on a high value as there might be a lot of latency \
/// Consider leaving this on a high value as there might be a lot of latency \
/// between you and the terminal (e.g. when you're connected via SSH).
pub max_timeout: Duration,
pub timeout: Duration,
}

impl Default for QueryOptions {
fn default() -> Self {
Self {
max_timeout: Duration::from_secs(1),
timeout: Duration::from_secs(1),
}
}
}

/// Queries the terminal for it's color scheme (foreground and background color).
#[doc = include_str!("../doc/caveats.md")]
pub fn color_scheme(options: QueryOptions) -> Result<ColorScheme> {
imp::color_scheme(options)
}

/// Queries the terminal for it's foreground color. \
/// If you also need the foreground color it is more efficient to use [`color_scheme`] instead.
#[doc = include_str!("../doc/caveats.md")]
pub fn foreground_color(options: QueryOptions) -> Result<Color> {
imp::foreground_color(options)
}

/// Queries the terminal for it's background color. \
/// If you also need the foreground color it is more efficient to use [`color_scheme`] instead.
#[doc = include_str!("../doc/caveats.md")]
pub fn background_color(options: QueryOptions) -> Result<Color> {
imp::background_color(options)
}
Expand Down
22 changes: 17 additions & 5 deletions src/os/macos/poll.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
use crate::{Error, Result};
use libc::{c_int, pselect, time_t, timespec, FD_ISSET, FD_SET};
use std::io;
use std::mem::zeroed;
use std::os::fd::{AsRawFd as _, BorrowedFd};
use std::ptr::{null, null_mut};
use std::time::Duration;
use terminal_trx::Transceive;
use thiserror::Error;

// macOS does not support polling /dev/tty using kqueue, so we have to
// resort to pselect/select. See https://nathancraddock.com/blog/macos-dev-tty-polling/.
pub(crate) fn poll_read(terminal: &dyn Transceive, timeout: Duration) -> Result<()> {
pub(crate) fn poll_read(terminal: BorrowedFd, timeout: Duration) -> io::Result<()> {
if timeout.is_zero() {
return Err(timed_out());
}

let fd = terminal.as_raw_fd();
let timespec = to_timespec(timeout);
// SAFETY: A zeroed fd_set is valid (FD_ZERO zeroes an existing fd_set so this state must be fine).
Expand All @@ -28,7 +32,7 @@ pub(crate) fn poll_read(terminal: &dyn Transceive, timeout: Duration) -> Result<
if FD_ISSET(fd, &readfds) {
Ok(())
} else {
Err(Error::Timeout(timeout))
Err(timed_out())
}
}
}
Expand All @@ -43,10 +47,18 @@ fn to_timespec(duration: Duration) -> timespec {
}
}

pub(super) fn to_io_result(value: c_int) -> io::Result<c_int> {
fn to_io_result(value: c_int) -> io::Result<c_int> {
if value == -1 {
Err(io::Error::last_os_error())
} else {
Ok(value)
}
}

fn timed_out() -> io::Error {
io::Error::new(io::ErrorKind::TimedOut, PollReadTimedOutError)
}

#[derive(Debug, Error)]
#[error("poll_read timed out")]
struct PollReadTimedOutError;
2 changes: 0 additions & 2 deletions src/os/unix/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
#[cfg(not(target_os = "macos"))]
mod poll;
#[cfg(not(target_os = "macos"))]
pub(crate) use poll::*;
21 changes: 17 additions & 4 deletions src/os/unix/poll.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
use crate::{Error, Result};
use mio::unix::SourceFd;
use mio::{Events, Interest, Poll, Token};
use std::io;
use std::os::fd::{AsRawFd as _, BorrowedFd};
use std::time::Duration;
use terminal_trx::Transceive;
use thiserror::Error;

pub(crate) fn poll_read(terminal: BorrowedFd, timeout: Duration) -> io::Result<()> {
if timeout.is_zero() {
return Err(timed_out());
}

pub(crate) fn poll_read(terminal: &dyn Transceive, timeout: Duration) -> Result<()> {
let mut poll = Poll::new()?;
let mut events = Events::with_capacity(1024);
let token = Token(0);
Expand All @@ -19,5 +24,13 @@ pub(crate) fn poll_read(terminal: &dyn Transceive, timeout: Duration) -> Result<
return Ok(());
}
}
Err(Error::Timeout(timeout))
Err(timed_out())
}

fn timed_out() -> io::Error {
io::Error::new(io::ErrorKind::TimedOut, PollReadTimedOutError)
}

#[derive(Debug, Error)]
#[error("poll_read timed out")]
struct PollReadTimedOutError;
69 changes: 0 additions & 69 deletions src/terminal.rs

This file was deleted.

Loading

0 comments on commit a08850d

Please sign in to comment.