Skip to content

Commit

Permalink
Merge pull request #1 from bash/windows
Browse files Browse the repository at this point in the history
Windows
  • Loading branch information
bash authored Jan 20, 2024
2 parents f2f1f4e + 727626c commit ca7e39d
Show file tree
Hide file tree
Showing 17 changed files with 334 additions and 55 deletions.
18 changes: 14 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ on:

env:
CARGO_TERM_COLOR: always
RUSTFLAGS: -Dwarnings

jobs:
build:
Expand All @@ -16,10 +17,19 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --all
run: cargo build
- name: Check format
run: cargo fmt --all -- --check
run: cargo fmt -- --check
- name: Run clippy
run: cargo clippy --workspace --all-targets --all-features -- --deny warnings
run: cargo clippy --all-targets --all-features -- --deny warnings
- name: Docs
run: cargo doc --features __docs
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- name: Run tests
run: cargo test --all
run: cargo test
3 changes: 2 additions & 1 deletion Cargo.lock

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

13 changes: 12 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,18 @@ thiserror = "1.0.56"
[target.'cfg(unix)'.dependencies]
libc = "0.2.151"
mio = { version = "0.8.10", features = ["os-poll", "os-ext"], default-features = false }
terminal-trx = { git = "https://github.com/bash/terminal-trx", branch = "windows" }
terminal-trx = { git = "https://github.com/bash/terminal-trx" }

[target.'cfg(windows)'.dependencies]
terminal-trx = { git = "https://github.com/bash/terminal-trx" }
windows-sys = { version = "0.52.0", features = ["Win32_System_Threading", "Win32_System_Console", "Win32_Foundation"] }

[features]
__docs = []
__test_readme = []

[package.metadata.docs.rs]
features = ["__docs"]

[workspace]
members = [
Expand Down
12 changes: 9 additions & 3 deletions doc/terminal-survey.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,20 @@
| ConEmu / Cmder | yes | no | no | - | - | - | - | 230724 stable |
| Mintty | yes | `rgb:ffff/ffff/ffff` | `rgb:ffff/ffff/ffff` | - | `xterm` | `mintty` | yes | 3.6.1 |

> [!note]
> Some Linux terminals are omitted since they all use the `vte` library behind the scenes. \
> Here's a non-exhaustive list: GNOME Terminal, (GNOME) Console, MATE Terminal, XFCE Terminal, (GNOME) Builder, (elementary) Terminal, LXTerminal.
<br>

**ℹ️ Note:**
Some Linux terminals are omitted since they all use the `vte` library behind the scenes. \
Here's a non-exhaustive list: GNOME Terminal, (GNOME) Console, MATE Terminal, XFCE Terminal, (GNOME) Builder, (elementary) Terminal, LXTerminal.

[^1]: But it sets `TERMINAL_EMULATOR=JetBrains-JediTerm` instead.

[^2]: But it provides a terminfo entry by adding `TERMINFO_DIRS`.

[^3]: But it sets `TERMINAL_NAME=contour` instead.

[^4]: But it sets `TERMINAL_VERSION_STRING` and `TERMINAL_VERSION_TRIPLE` instead.

[^5]: But it can be recognized by `WT_SESSION` instead.


Expand Down
24 changes: 24 additions & 0 deletions doc/windows-read-with-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Waiting for Console Input with a Timeout
This can be achieved using `WaitForSingleObject`.

```rust
use crate::{Error, Result};
use std::io;
use std::os::windows::io::AsRawHandle;
use std::time::Duration;
use terminal_trx::Transceive;
use windows_sys::Win32::Foundation::{WAIT_ABANDONED, WAIT_OBJECT_0, WAIT_TIMEOUT};
use windows_sys::Win32::System::Threading::WaitForSingleObject;

pub(crate) fn poll_read(terminal: &dyn Transceive, timeout: Duration) -> Result<()> {
let handle = terminal.input_buffer_handle();
match unsafe {
WaitForSingleObject(handle.as_raw_handle() as isize, timeout.as_millis() as u32)
} {
// The state of the specified object is signaled.
WAIT_OBJECT_0 => Ok(()),
WAIT_ABANDONED | WAIT_TIMEOUT => Err(Error::Timeout(timeout)),
_ => Err(Error::Io(io::Error::last_os_error())),
}
}
```
15 changes: 15 additions & 0 deletions doc/windows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Windows
## Windows Terminal
The Win32 API provides access to the console foreground and background colors.
However, this currently does not work in Windows Terminal. [Incorrect colors are reported] as a result.

Hopefully Windows Terminal [will support] querying for the colors using the OSC sequences.

## Other Terminals
Terminals relying on [conpty] that support OSC sequences on other platforms
do not support them of Windows because [conhost intercepts these OSC sequences].

[Incorrect colors are reported]: https://github.com/microsoft/terminal/issues/10639
[will support]: https://github.com/microsoft/terminal/issues/3718
[conpty]: https://learn.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session
[conhost intercepts these OSC sequences]: https://github.com/microsoft/terminal/issues/1173
40 changes: 37 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,51 @@
# term-color
TODO
Determines the background and foreground color of the terminal
using the `OSC 10` and `OSC 11` terminal sequence.
On Windows, the colors are queried using the Win32 Console API.

This is useful for answering the question *"Is this terminal dark or light?"*.

## Example
```rust,no_run
use term_color::{background_color, QueryOptions};
let bg = background_color(QueryOptions::default());
// Perceived lightness is a value between 0 (black) and 100 (white)
let is_light = bg.map(|c| c.perceived_lightness() >= 50).unwrap_or_default();
```

## Wishlist
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.

* [xterm-query]: Use `mio` to wait for the terminal's response with a timeout.
* [termbg]: Lists a lot of terminals which served as a good starting point for me to test terminals as well.
* [macOS doesn't like polling /dev/tty][macos-dev-tty] by Nathan Craddock
* [This excellent answer on Stack Overflow][perceived-lightness] for determining the perceived lightness of a color.

## License
Licensed under either of

* Apache License, Version 2.0
([license-apache.txt](license-apache.txt) or http://www.apache.org/licenses/LICENSE-2.0)
([license-apache.txt](license-apache.txt) or <http://www.apache.org/licenses/LICENSE-2.0>)
* MIT license
([license-mit.txt](license-mit.txt) or http://opensource.org/licenses/MIT)
([license-mit.txt](license-mit.txt) or <http://opensource.org/licenses/MIT>)

at your option.

## Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
dual licensed as above, without any additional terms or conditions.

[xterm-query]: https://github.com/Canop/xterm-query
[termbg]: https://github.com/dalance/termbg
[macos-dev-tty]: https://nathancraddock.com/blog/macos-dev-tty-polling/
[perceived-lightness]: https://stackoverflow.com/a/56678483
11 changes: 11 additions & 0 deletions src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ impl Color {

impl Color {
/// Parses an X11 color (see `man xparsecolor`).
#[cfg(unix)]
pub(crate) fn parse_x11(input: &str) -> Option<Self> {
let raw_parts = input.strip_prefix("rgb:")?;
let mut parts = raw_parts.split('/');
Expand All @@ -30,8 +31,18 @@ impl Color {
let blue = parse_channel(parts.next()?)?;
Some(Color { red, green, blue })
}

#[cfg(windows)]
pub(crate) fn from_8bit(red: u8, green: u8, blue: u8) -> Color {
Color {
red: (red as u16) << 8,
green: (green as u16) << 8,
blue: (blue as u16) << 8,
}
}
}

#[cfg(unix)]
fn parse_channel(input: &str) -> Option<u16> {
let len = input.len();
// From the xparsecolor man page:
Expand Down
75 changes: 57 additions & 18 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
//! Determines the background and foreground color of the terminal
//! using the `OSC 10` and `OSC 11` terminal sequence.
//!
//! On Windows, the colors are queried using the Win32 Console API.
//!
//! This is useful for answering the question *"Is this terminal dark or light?"*.
//!
//! ## Features
//! * Background and foreground color detection.
//! * Uses a variable timeout (for situations with high latency such as an SSH connection).
//! * *Correct* perceived lightness calculation.
//! * Works even if all of stderr, stdout and stdin are redirected.
//! * Safely restores the terminal from raw mode even if the library panicks.
//! * Safely restores the terminal from raw mode even if the library errors or panicks.
//! * Does not send any escape sequences if `TERM=dumb`.
//!
//! ## Example: Test If the Terminal Uses a Dark Background
//! ```no_run
//! use term_color::{background_color, QueryOptions};
//!
//! let bg = background_color(QueryOptions::default());
//! // Perceived lightness is a value between 0 (black) and 100 (white)
//! let is_light = bg.map(|c| c.perceived_lightness() >= 50).unwrap_or_default();
//! ```
//!
//! ## Terminals
//! The following terminals have known support or non-support for
//! querying for the background/foreground colors.
//!
//! Note that terminals that support the relevant terminal
//! sequences automatically work with this library even if they
//! are not explicitly listed below.
//!
//! ## Supported Terminals
//! ### Supported
//! * macOS Terminal
//! * iTerm2
//! * Alacritty
Expand All @@ -19,49 +41,66 @@
//! * Console
//! * foot
//! * xterm
//! * tmux (next-3.4)
//! * Windows Console (conhost)
//!
//! ## Example: Test If the Terminal Uses a Dark Background
//! ```no_run
//! use term_color::{background_color, QueryOptions};
//!
//! let bg = background_color(QueryOptions::default());
//! // Perceived lightness is a value between 0 (black) and 100 (white)
//! let is_light = bg.map(|c| c.perceived_lightness() >= 50).unwrap_or_default();
//! ```
//! ### Unsupported
//! * linux
//! * Jetbrains Fleet
//! * Windows Terminal
//!
//! ## 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.
//! 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 Libraries
//! * termbg: TODO
//! * dark-light: TODO
//! ## Comparison with Other Crates
//! ### [termbg]
//! * Is hardcoded to use stdin/stderr for communicating with the terminal. \
//! This means that it does not work if some or all of these streams are redirected.
//! * Pulls in an async runtime for the timeout.
//!
//! ### [terminal-light]
//! * Is hardcoded to use stdin/stdout for communicating with the terminal.
//! * Does not report the colors, only the color's luma.
//!
//! [terminal-survey]: https://github.com/bash/term-color/blob/main/doc/terminal-survey.md
//! [termbg]: https://docs.rs/termbg
//! [terminal-light]: https://docs.rs/terminal-light
use std::io;
use std::time::Duration;
use thiserror::Error;

mod color;
#[cfg(unix)]
mod os;

#[cfg(unix)]
mod terminal;
#[cfg(windows)]
mod winapi;
#[cfg(unix)]
mod xterm;

#[cfg(windows)]
use winapi as imp;
#[cfg(unix)]
use xterm as imp;

#[cfg(not(unix))]
#[cfg(not(any(unix, windows)))]
use unsupported as imp;

#[cfg(feature = "__docs")]
#[doc = include_str!("../doc/terminal-survey.md")]
pub mod terminal_survey {}

#[cfg(feature = "__test_readme")]
#[doc = include_str!("../readme.md")]
pub mod readme {}

pub use color::*;

/// Result used by this library.
Expand Down Expand Up @@ -123,7 +162,7 @@ pub fn background_color(options: QueryOptions) -> Result<Color> {
imp::background_color(options)
}

#[cfg(not(unix))]
#[cfg(not(any(unix, windows)))]
mod unsupported {
use crate::{Color, Error, QueryOptions, Result};

Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::error::Error;

use term_color::QueryOptions;

fn main() -> Result<(), Box<dyn Error>> {
let color = term_color::background_color(QueryOptions::default())?;
dbg!(color.perceived_lightness());
dbg!(color.perceived_lightness() <= 50);
dbg!(color);
dbg!(term_color::foreground_color(QueryOptions::default())?);
Ok(())
}
17 changes: 13 additions & 4 deletions src/os/macos/poll.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use super::super::to_io_result;
use crate::{Error, Result};
use libc::{timespec, FD_ISSET};
use std::os::fd::RawFd;
use libc::{c_int, timespec, FD_ISSET};
use std::io;
use std::time::Duration;
use std::{mem, ptr};
use terminal_trx::Transceive;

// 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(fd: RawFd, timeout: Duration) -> Result<()> {
pub(crate) fn poll_read(terminal: &dyn Transceive, timeout: Duration) -> Result<()> {
let fd = terminal.as_raw_fd();
let mut readfds = unsafe { std::mem::zeroed() };
let timespec = to_timespec(timeout);
unsafe { libc::FD_SET(fd, &mut readfds) };
Expand Down Expand Up @@ -41,3 +42,11 @@ const fn to_timespec(duration: Duration) -> timespec {
}
ts
}

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

0 comments on commit ca7e39d

Please sign in to comment.