Compare commits

...

14 Commits

158 changed files with 1381 additions and 14440 deletions

1425
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -16,7 +16,7 @@ members = [
"crates/qrcode/keyfork-zbar-sys",
"crates/util/keyfork-bin",
"crates/util/keyfork-bug",
"crates/util/keyfork-crossterm",
"crates/util/keyfork-crossterm-ioctl-shim",
"crates/util/keyfork-entropy",
"crates/util/keyfork-frame",
"crates/util/keyfork-mnemonic",
@ -26,6 +26,29 @@ members = [
"crates/tests",
]
[workspace.lints.rust]
missing_docs = { level = "warn" }
[workspace.lints.clippy]
all = { level = "deny", priority = -1 }
pedantic = { level = "warn", priority = -1 }
missing_errors_doc = { level = "warn" }
missing_panics_doc = { level = "warn" }
# used often in tests
wildcard_imports = { level = "allow"}
# annoying
must_use_candidate = "allow"
return_self_not_must_use = "allow"
# sometimes i like the logical flow of keeping things in an "else"
redundant_else = "allow"
# i hate using `.unwrap_or_else(|| keyfork_bug::bug!())`
expect_fun_call = "allow"
[workspace.dependencies]
# Keyfork dependencies
@ -41,7 +64,6 @@ keyfork-zbar = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar", registr
keyfork-zbar-sys = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar-sys", registry = "distrust", default-features = false }
keyfork-bin = { version = "0.1.0", path = "crates/util/keyfork-bin", registry = "distrust", default-features = false }
keyfork-bug = { version = "0.1.1", path = "crates/util/keyfork-bug", registry = "distrust", default-features = false }
keyfork-crossterm = { version = "0.27.1", path = "crates/util/keyfork-crossterm", registry = "distrust", default-features = false }
keyfork-entropy = { version = "0.1.1", path = "crates/util/keyfork-entropy", registry = "distrust", default-features = false }
keyfork-frame = { version = "0.1.0", path = "crates/util/keyfork-frame", registry = "distrust", default-features = false }
keyfork-mnemonic = { version = "0.4.0", path = "crates/util/keyfork-mnemonic", registry = "distrust", default-features = false }
@ -70,7 +92,7 @@ serde_json = "1.0.111"
# Misc.
anyhow = "1.0.79"
hex-literal = "0.4.1"
hex-literal = "1.0.0"
image = { version = "0.25.2", default-features = false }
thiserror = "1.0.56"
tokio = "1.35.1"
@ -84,4 +106,3 @@ debug = true
[profile.dev.package.keyfork-qrcode]
opt-level = 3
debug = true

View File

@ -34,7 +34,7 @@ review:
$(eval HEAD_REF_PARSED := $(shell git rev-parse $(HEAD_REF)))
@echo "Ensuring current HEAD_REF is not BASE_REF"
test "$(BASE_REF_PARSED)" != "$(HEAD_REF_PARSED)"
@echo "Verifying if any changes happened in Cargo.lock that require review; otherwise, use `git difftool` directly"
@echo "Verifying if any changes happened in Cargo.lock that require review; otherwise, use: git difftool"
test "$(shell git show $(BASE_REF_PARSED):Cargo.lock | sha256sum)" != "$(shell git show $(HEAD_REF_PARSED):Cargo.lock | sha256sum)"
$(eval TEMP_REPO := $(shell mktemp -d))
$(call clone-repo,$(TEMP_REPO),$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST)))),$(BASE_REF_PARSED))

1
clippy.toml Normal file
View File

@ -0,0 +1 @@
doc-valid-idents = ["OpenPGP", ".."]

View File

@ -4,6 +4,9 @@ version = "0.2.2"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]

View File

@ -26,7 +26,7 @@
//!
//! ### Request: Derive Key
//!
//! The client creates a derivation path of at least two indices and requests a derived XPrv
//! The client creates a derivation path of at least two indices and requests a derived `XPrv`
//! (Extended Private Key) from the server.
//!
//! ```rust
@ -68,7 +68,7 @@
//! ## Extended Private Keys
//!
//! Keyfork doesn't need to be continuously called once a key has been derived. Once an Extended
//! Private Key (often shortened to "XPrv") has been created, further derivations can be performed.
//! Private Key (often shortened to `XPrv`) has been created, further derivations can be performed.
//! The tests for this library ensure that all levels of Keyfork derivation beyond the required two
//! will be derived similarly between the server and the client.
//!
@ -117,7 +117,7 @@
//!
//! ## Testing Infrastructure
//!
//! In tests, the `keyforkd::test_util` module and TestPrivateKeys can be used. These provide
//! In tests, the `keyforkd::test_util` module and `TestPrivateKeys` can be used. These provide
//! useful utilities for writing tests that interact with the Keyfork Server without needing to
//! manually create the server for the purpose of the test. The `run_test` method can be used to
//! run a test, which can handle both returning errors and correctly translating panics (though,
@ -199,6 +199,10 @@ pub enum Error {
#[error("Socket was unable to connect to {1}: {0} (make sure keyforkd is running)")]
Connect(std::io::Error, PathBuf),
/// The path of the derived key was of an invalid length.
#[error("Derived key path is of invalid length")]
InvalidPathLength(#[from] std::num::TryFromIntError),
/// Data could not be written to, or read from, the socket.
#[error("Could not write to or from the socket: {0}")]
Io(#[from] std::io::Error),
@ -237,12 +241,9 @@ pub fn get_socket() -> Result<UnixStream, Error> {
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
.collect::<HashMap<String, String>>();
let mut socket_path: PathBuf;
#[allow(clippy::single_match_else)]
match socket_vars.get("KEYFORKD_SOCKET_PATH") {
Some(occupied) => {
if let Some(occupied) = socket_vars.get("KEYFORKD_SOCKET_PATH") {
socket_path = PathBuf::from(occupied);
}
None => {
} else {
socket_path = PathBuf::from(
socket_vars
.get("XDG_RUNTIME_DIR")
@ -250,7 +251,6 @@ pub fn get_socket() -> Result<UnixStream, Error> {
);
socket_path.extend(["keyforkd", "keyforkd.sock"]);
}
}
UnixStream::connect(&socket_path).map_err(|e| Error::Connect(e, socket_path))
}
@ -266,7 +266,7 @@ pub struct Client {
impl Client {
/// Create a new client from a given already-connected [`UnixStream`]. This function is
/// provided in case a specific UnixStream has to be used; otherwise,
/// provided in case a specific `UnixStream` has to be used; otherwise,
/// [`Client::discover_socket`] should be preferred.
///
/// # Examples
@ -344,7 +344,7 @@ impl Client {
return Err(Error::InvalidResponse);
}
let depth = path.len() as u8;
let depth = u8::try_from(path.len())?;
ExtendedPrivateKey::from_parts(&d.data, depth, d.chain_code)
.map_err(|_| Error::InvalidKey)
}

View File

@ -9,14 +9,13 @@ use std::{os::unix::net::UnixStream, str::FromStr};
fn secp256k1_test_suite() {
use k256::SecretKey;
let tests = test_data()
.unwrap()
.remove("secp256k1")
.unwrap();
let tests = test_data().unwrap().remove("secp256k1").unwrap();
for seed_test in tests {
let seed = seed_test.seed;
run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
run_test(
&seed,
move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
for test in seed_test.tests {
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
@ -60,7 +59,8 @@ fn secp256k1_test_suite() {
assert_eq!(&response.data, test.private_key.as_slice());
}
Ok(())
})
},
)
.unwrap();
}
}

View File

@ -4,6 +4,9 @@ version = "0.2.0"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -4,6 +4,9 @@ version = "0.1.4"
edition = "2021"
license = "AGPL-3.0-only"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
@ -27,7 +30,7 @@ tokio = { workspace = true, features = ["io-util", "macros", "rt", "io-std", "ne
tracing = { version = "0.1.37", optional = true }
tracing-error = { version = "0.2.0", optional = true }
tracing-subscriber = { version = "0.3.17", optional = true, features = ["env-filter"] }
tower = { version = "0.4.13", features = ["tokio", "util"] }
tower = { version = "0.5.0", features = ["tokio", "util"], default-features = false }
# Personally audited
thiserror = { workspace = true }

View File

@ -89,14 +89,10 @@ pub async fn start_and_run_server(mnemonic: Mnemonic) -> Result<(), Box<dyn std:
let runtime_vars = std::env::vars()
.filter(|(key, _)| ["XDG_RUNTIME_DIR", "KEYFORKD_SOCKET_PATH"].contains(&key.as_str()))
.collect::<HashMap<String, String>>();
let mut runtime_path: PathBuf;
#[allow(clippy::single_match_else)]
match runtime_vars.get("KEYFORKD_SOCKET_PATH") {
Some(occupied) => {
runtime_path = PathBuf::from(occupied);
}
None => {
runtime_path = PathBuf::from(
let runtime_path = if let Some(occupied) = runtime_vars.get("KEYFORKD_SOCKET_PATH") {
PathBuf::from(occupied)
} else {
let mut runtime_path = PathBuf::from(
runtime_vars
.get("XDG_RUNTIME_DIR")
.ok_or(KeyforkdError::NoSocketPath)?,
@ -108,8 +104,8 @@ pub async fn start_and_run_server(mnemonic: Mnemonic) -> Result<(), Box<dyn std:
tokio::fs::create_dir(&runtime_path).await?;
}
runtime_path.push("keyforkd.sock");
}
}
runtime_path
};
#[cfg(feature = "tracing")]
debug!(

View File

@ -162,6 +162,9 @@ mod tests {
.call(content.clone())
.await
.unwrap();
assert_eq!(result, serialize(&Result::<Test, Infallible>::Ok(test)).unwrap());
assert_eq!(
result,
serialize(&Result::<Test, Infallible>::Ok(test)).unwrap()
);
}
}

View File

@ -49,7 +49,8 @@ impl IsDisconnect for EncodeError {
}
impl UnixServer {
/// Bind a socket to the given `address` and create a [`UnixServer`]. This function also creates a ctrl_c handler to automatically clean up the socket file.
/// Bind a socket to the given `address` and create a [`UnixServer`]. This function also
/// creates a `ctrl_c` handler to automatically clean up the socket file.
///
/// # Errors
/// This function may return an error if the socket can't be bound.

View File

@ -39,7 +39,10 @@ impl Keyforkd {
/// Create a new instance of Keyfork from a given seed.
pub fn new(seed: Vec<u8>) -> Self {
if seed.len() < 16 {
warn!("Entropy size is lower than 128 bits: {} bits.", seed.len() * 8);
warn!(
"Entropy size is lower than 128 bits: {} bits.",
seed.len() * 8
);
}
Self {
seed: Arc::new(seed),
@ -77,11 +80,11 @@ impl Service<Request> for Keyforkd {
.iter()
.take(2)
.enumerate()
.find(|(_, index)| {
!index.is_hardened()
})
.find(|(_, index)| !index.is_hardened())
{
return Err(DerivationError::InvalidDerivationPath(i, unhardened_index.inner()).into())
return Err(
DerivationError::InvalidDerivationPath(i, unhardened_index.inner()).into(),
);
}
#[cfg(feature = "tracing")]
@ -111,10 +114,7 @@ mod tests {
#[tokio::test]
async fn properly_derives_secp256k1() {
let tests = test_data()
.unwrap()
.remove("secp256k1")
.unwrap();
let tests = test_data().unwrap().remove("secp256k1").unwrap();
for per_seed in tests {
let seed = &per_seed.seed;
@ -174,7 +174,7 @@ mod tests {
}
}
#[should_panic]
#[should_panic(expected = "InvalidDerivationLength(0)")]
#[tokio::test]
async fn errors_on_no_path() {
let tests = [(
@ -200,7 +200,7 @@ mod tests {
}
}
#[should_panic]
#[should_panic(expected = "InvalidDerivationLength(1)")]
#[tokio::test]
async fn errors_on_short_path() {
let tests = [(

View File

@ -89,8 +89,11 @@ where
.expect(bug!("couldn't send server start signal"));
let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new())
.service(Keyforkd::new(seed.to_vec()));
server.run(service).await.expect(bug!("Unable to start service"));
.service(Keyforkd::new(seed.clone()));
server
.run(service)
.await
.expect(bug!("Unable to start service"));
}
});

View File

@ -4,6 +4,9 @@ version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-only"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -4,6 +4,9 @@ version = "0.1.2"
edition = "2021"
license = "AGPL-3.0-only"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -46,7 +46,10 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::discover_socket()?;
let request = DerivationRequest::new(algo, &path);
let response = client.request(&request.into())?;
println!("{}", smex::encode(DerivationResponse::try_from(response)?.data));
println!(
"{}",
smex::encode(DerivationResponse::try_from(response)?.data)
);
Ok(())
}

View File

@ -4,6 +4,9 @@ version = "0.1.5"
edition = "2021"
license = "AGPL-3.0-only"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default = ["bin"]

View File

@ -74,7 +74,7 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
///
/// # Errors
/// The function may error for any condition mentioned in [`Error`].
pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
pub fn derive(xprv: &XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
let primary_key_flags = match keys.first() {
Some(kf) if kf.for_certification() => kf,
_ => return Err(Error::NotCert),

View File

@ -2,8 +2,8 @@
use std::{env, process::ExitCode, str::FromStr};
use keyfork_derive_util::DerivationPath;
use keyfork_derive_path_data::paths;
use keyfork_derive_util::DerivationPath;
use keyforkd_client::Client;
use ed25519_dalek::SigningKey;
@ -82,7 +82,10 @@ fn validate(
let index = paths::OPENPGP.inner().first().unwrap();
let path = DerivationPath::from_str(path)?;
assert!(path.len() >= 2, "Expected path of at least m/{index}/account_id'");
assert!(
path.len() >= 2,
"Expected path of at least m/{index}/account_id'"
);
let given_index = path.iter().next().expect("checked .len() above");
assert_eq!(
@ -117,7 +120,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
.map(|kt| kt.inner().clone())
.collect::<Vec<_>>();
let cert = keyfork_derive_openpgp::derive(derived_xprv, subkeys.as_slice(), &default_userid)?;
let cert = keyfork_derive_openpgp::derive(&derived_xprv, subkeys.as_slice(), &default_userid)?;
let mut w = Writer::new(std::io::stdout(), Kind::SecretKey)?;

View File

@ -4,6 +4,9 @@ version = "0.1.3"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -2,7 +2,7 @@
#![allow(clippy::unreadable_literal)]
use once_cell::sync::Lazy;
use std::sync::LazyLock;
use keyfork_derive_util::{DerivationIndex, DerivationPath};
@ -11,7 +11,7 @@ pub mod paths {
use super::*;
/// The default derivation path for OpenPGP.
pub static OPENPGP: Lazy<DerivationPath> = Lazy::new(|| {
pub static OPENPGP: LazyLock<DerivationPath> = LazyLock::new(|| {
DerivationPath::default().chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"\x00pgp"),
true,
@ -19,7 +19,7 @@ pub mod paths {
});
/// The derivation path for OpenPGP certificates used for sharding.
pub static OPENPGP_SHARD: Lazy<DerivationPath> = Lazy::new(|| {
pub static OPENPGP_SHARD: LazyLock<DerivationPath> = LazyLock::new(|| {
DerivationPath::default()
.chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"\x00pgp"),
@ -32,7 +32,7 @@ pub mod paths {
});
/// The derivation path for OpenPGP certificates used for disaster recovery.
pub static OPENPGP_DISASTER_RECOVERY: Lazy<DerivationPath> = Lazy::new(|| {
pub static OPENPGP_DISASTER_RECOVERY: LazyLock<DerivationPath> = LazyLock::new(|| {
DerivationPath::default()
.chain_push(DerivationIndex::new_unchecked(
u32::from_be_bytes(*b"\x00pgp"),

View File

@ -4,6 +4,9 @@ version = "0.2.2"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]

View File

@ -52,7 +52,7 @@ pub struct VariableLengthSeed<'a> {
}
impl<'a> VariableLengthSeed<'a> {
/// Create a new VariableLengthSeed.
/// Create a new `VariableLengthSeed`.
///
/// # Examples
/// ```rust
@ -167,6 +167,7 @@ where
/// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let xprv = ExtendedPrivateKey::<PrivateKey>::new(*seed);
/// ```
#[allow(clippy::needless_pass_by_value)]
pub fn new(seed: impl as_private_key::AsPrivateKey) -> Result<Self> {
Self::new_internal(seed.as_private_key())
}
@ -189,7 +190,10 @@ where
}
}
assert!(has_any_nonzero, bug!("hmac function returned all-zero master key"));
assert!(
has_any_nonzero,
bug!("hmac function returned all-zero master key")
);
Self::from_parts(
private_key
@ -223,13 +227,11 @@ where
/// ```
pub fn from_parts(key: &[u8; 32], depth: u8, chain_code: [u8; 32]) -> Result<Self> {
match K::from_bytes(key) {
Ok(key) => {
Ok(Self {
Ok(key) => Ok(Self {
private_key: key,
depth,
chain_code,
})
}
}),
Err(_) => Err(Error::InvalidKey),
}
}

View File

@ -117,7 +117,7 @@ mod tests {
use std::str::FromStr;
#[test]
#[should_panic]
#[should_panic(expected = "IndexTooLarge")]
fn fails_on_high_index() {
DerivationIndex::new(0x8000_0001, false).unwrap();
}
@ -163,7 +163,7 @@ mod tests {
}
#[test]
#[should_panic]
#[should_panic(expected = "IndexTooLarge")]
fn from_str_fails_on_high_index() {
DerivationIndex::from_str(&0x8000_0001u32.to_string()).unwrap();
}

View File

@ -1,4 +1,4 @@
#![allow(clippy::module_name_repetitions, clippy::must_use_candidate)]
#![allow(clippy::module_name_repetitions)]
#![doc = include_str!("../README.md")]
pub mod extended_key;
@ -17,7 +17,10 @@ pub mod public_key;
mod tests;
#[doc(inline)]
pub use crate::extended_key::{private_key::{ExtendedPrivateKey, Error as XPrvError, VariableLengthSeed}, public_key::{ExtendedPublicKey, Error as XPubError}};
pub use crate::extended_key::{
private_key::{Error as XPrvError, ExtendedPrivateKey, VariableLengthSeed},
public_key::{Error as XPubError, ExtendedPublicKey},
};
pub use crate::{
index::{DerivationIndex, Error as IndexError},

View File

@ -11,7 +11,7 @@ pub enum Error {
/// The path could not be parsed due to a bad prefix. Paths must be in the format:
///
/// m [/ index [']]+
/// `m [/ index [']]+`
///
/// The prefix for the path must be `m`, and all indices must be integers between 0 and
/// 2^31.
@ -35,8 +35,8 @@ impl DerivationPath {
self.path.iter()
}
/// The amount of segments in the DerivationPath. For consistency, a [`usize`] is returned, but
/// BIP-0032 dictates that the depth should be no larger than `255`, [`u8::MAX`].
/// The amount of segments in the [`DerivationPath`]. For consistency, a [`usize`] is returned,
/// but BIP-0032 dictates that the depth should be no larger than `255`, [`u8::MAX`].
pub fn len(&self) -> usize {
self.path.len()
}
@ -134,7 +134,7 @@ mod tests {
use std::str::FromStr;
#[test]
#[should_panic]
#[should_panic(expected = "UnknownPathPrefix")]
fn requires_master_path() {
DerivationPath::from_str("1234/5678'").unwrap();
}

View File

@ -110,7 +110,7 @@ impl PublicKey for k256::PublicKey {
fn derive_child(&self, other: PrivateKeyBytes) -> Result<Self, Self::Err> {
use k256::elliptic_curve::ScalarPrimitive;
use k256::{Secp256k1, Scalar};
use k256::{Scalar, Secp256k1};
// Construct a scalar from bytes
let scalar = ScalarPrimitive::<Secp256k1>::from_bytes(&other.into());
@ -156,7 +156,7 @@ pub struct TestPublicKey {
}
impl TestPublicKey {
/// Create a new TestPublicKey from the given bytes.
/// Create a new [`TestPublicKey`] from the given bytes.
#[allow(dead_code)]
pub fn from_bytes(b: &[u8]) -> Self {
Self {

View File

@ -13,10 +13,7 @@ use keyfork_slip10_test_data::{test_data, Test};
fn secp256k1() {
use k256::SecretKey;
let tests = test_data()
.unwrap()
.remove("secp256k1")
.unwrap();
let tests = test_data().unwrap().remove("secp256k1").unwrap();
for per_seed in tests {
let seed = &per_seed.seed;
@ -105,7 +102,7 @@ fn ed25519() {
#[cfg(feature = "ed25519")]
#[test]
#[should_panic]
#[should_panic(expected = "HardenedDerivationRequired")]
fn panics_with_unhardened_derivation() {
use ed25519_dalek::SigningKey;
@ -117,7 +114,7 @@ fn panics_with_unhardened_derivation() {
#[cfg(feature = "ed25519")]
#[test]
#[should_panic]
#[should_panic(expected = "Depth")]
fn panics_at_depth() {
use ed25519_dalek::SigningKey;

View File

@ -4,6 +4,9 @@ version = "0.3.4"
edition = "2021"
license = "AGPL-3.0-only"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]

View File

@ -35,7 +35,11 @@ fn run() -> Result<()> {
let openpgp = OpenPGP;
let prompt_handler = default_handler()?;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file, prompt_handler)?;
let bytes = openpgp.decrypt_all_shards_to_secret(
key_discovery.as_deref(),
messages_file,
prompt_handler,
)?;
print!("{}", smex::encode(bytes));
Ok(())

View File

@ -8,7 +8,7 @@ use std::{
};
use keyfork_prompt::default_handler;
use keyfork_shard::{Format, openpgp::OpenPGP};
use keyfork_shard::{openpgp::OpenPGP, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -35,7 +35,11 @@ fn run() -> Result<()> {
let openpgp = OpenPGP;
let prompt_handler = default_handler()?;
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file, prompt_handler)?;
openpgp.decrypt_one_shard_for_transport(
key_discovery.as_deref(),
messages_file,
prompt_handler,
)?;
Ok(())
}

View File

@ -1,9 +1,6 @@
//! Combine OpenPGP shards using remote transport and output the hex-encoded secret.
use std::{
env,
process::ExitCode,
};
use std::{env, process::ExitCode};
use keyfork_shard::remote_decrypt;
@ -16,7 +13,7 @@ fn run() -> Result<()> {
match args.as_slice() {
[] => (),
_ => panic!("Usage: {program_name}"),
};
}
let mut bytes = vec![];
remote_decrypt(&mut bytes)?;

View File

@ -2,7 +2,7 @@
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
use keyfork_shard::{Format, openpgp::OpenPGP};
use keyfork_shard::{openpgp::OpenPGP, Format};
#[derive(Clone, Debug)]
enum Error {
@ -52,7 +52,13 @@ fn run() -> Result<()> {
let openpgp = OpenPGP;
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
openpgp.shard_and_encrypt(
threshold,
max,
&input,
key_discovery.as_path(),
std::io::stdout(),
)?;
Ok(())
}

View File

@ -1,5 +1,4 @@
#![doc = include_str!("../README.md")]
#![allow(clippy::expect_fun_call)]
use std::{
io::{Read, Write},
@ -92,9 +91,10 @@ pub trait KeyDiscovery<F: Format + ?Sized> {
/// # Errors
/// The method may return an error if private keys could not be loaded from the given
/// discovery mechanism. Keys may exist off-system (such as with smartcards), in which case the
/// PrivateKeyData type of the asssociated format should be either `()` (if the keys may never
/// exist on-system) or an empty container (such as an empty Vec); in either case, this method
/// _must not_ return an error if keys are accessible but can't be transferred into memory.
/// `PrivateKeyData` type of the asssociated format should be either `()` (if the keys may
/// never exist on-system) or an empty container (such as an empty Vec); in either case, this
/// method _must not_ return an error if keys are accessible but can't be transferred into
/// memory.
fn discover_private_keys(&self) -> Result<F::PrivateKeyData, F::Error>;
}
@ -121,8 +121,8 @@ pub trait Format {
/// Format a header containing necessary metadata. Such metadata contains a version byte, a
/// threshold byte, a public version of the [`Format::SigningKey`], and the public keys used to
/// encrypt shards. The public keys must be kept _in order_ to the encrypted shards. Keyfork
/// will use the same key_data for both, ensuring an iteration of this method will match with
/// iterations in methods called later.
/// will use the same `key_data` for both, ensuring an iteration of this method will match
/// with iterations in methods called later.
///
/// # Errors
/// The method may return an error if encryption to any of the public keys fails.
@ -263,6 +263,7 @@ pub trait Format {
/// The method may return an error if a share can't be decrypted. The method will not return an
/// error if the camera is inaccessible or if a hardware error is encountered while scanning a
/// QR code; instead, a mnemonic prompt will be used.
#[allow(clippy::too_many_lines)]
fn decrypt_one_shard_for_transport(
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
@ -326,14 +327,14 @@ pub trait Format {
if choice == RetryScanMnemonic::Continue {
break;
}
};
}
}
}
// if QR code scanning failed or was unavailable, read from a set of mnemonics
let their_pubkey = match pubkey_data {
Some(pubkey) => pubkey,
None => {
let their_pubkey = if let Some(pubkey) = pubkey_data {
pubkey
} else {
let validator = MnemonicValidator {
word_length: Some(WordLength::Count(24)),
};
@ -347,20 +348,6 @@ pub trait Format {
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?
/*
prompt
.lock()
.expect(bug!(POISONED_MUTEX))
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?
*/
}
};
// create our shared key
@ -401,7 +388,6 @@ pub trait Format {
// NOTE: Previous versions of Keyfork Shard would modify the padding bytes to avoid
// duplicate mnemonic words. This version does not include that, and instead uses a
// repeated length byte.
#[allow(clippy::cast_possible_truncation)]
let mut plaintext_bytes = [u8::try_from(payload.len()).expect(bug!(
"previously asserted length must be < {PLAINTEXT_LENGTH}",
PLAINTEXT_LENGTH = PLAINTEXT_LENGTH
@ -481,11 +467,11 @@ pub trait Format {
"must have less than u8::MAX public keys"
);
assert_eq!(
max,
public_keys.len() as u8,
max as usize,
public_keys.len(),
"max must be equal to amount of public keys"
);
let max = public_keys.len() as u8;
let max = u8::try_from(public_keys.len()).expect(bug!("invalid max: {max}", max = max));
assert!(max >= threshold, "threshold must not exceed max keys");
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
@ -547,6 +533,7 @@ static QRCODE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
/// # Panics
/// The function may panic if it is given payloads generated using a version of Keyfork that is
/// incompatible with the currently running version.
#[allow(clippy::too_many_lines)]
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = keyfork_prompt::default_handler()?;
@ -627,13 +614,13 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
if choice == RetryScanMnemonic::Continue {
break;
}
};
}
}
}
let (pubkey, payload) = match (pubkey_data, payload_data) {
(Some(pubkey), Some(payload)) => (pubkey, payload),
_ => {
let (pubkey, payload) = if let Some((pubkey, payload)) = pubkey_data.zip(payload_data) {
(pubkey, payload)
} else {
let validator = MnemonicSetValidator {
word_lengths: [24, 39],
};
@ -650,7 +637,6 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
.map_err(|_| InvalidData)?;
let payload = payload_mnemonic.to_bytes();
(pubkey, payload)
}
};
assert_eq!(
@ -677,17 +663,14 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
let payload = shared_key.decrypt(nonce, payload.as_slice())?;
assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version");
match &mut iter_count {
Some(n) => {
if let Some(n) = &mut iter_count {
// Must be > 0 to start loop, can't go lower
*n -= 1;
}
None => {
} else {
// NOTE: Should always be >= 1, < 256 due to Shamir constraints
threshold = payload[1];
let _ = iter_count.insert(threshold - 1);
}
}
let payload_len = payload.last().expect(bug!("payload should not be empty"));
shares.push(payload[HUNK_OFFSET..usize::from(*payload_len)].to_vec());

View File

@ -1,16 +1,15 @@
//! OpenPGP Shard functionality.
#![allow(clippy::expect_fun_call)]
use std::{
collections::HashMap,
io::{Read, Write},
path::Path,
rc::Rc,
str::FromStr,
sync::Mutex,
rc::Rc,
};
use blahaj::Share;
use keyfork_bug::bug;
use keyfork_derive_openpgp::{
derive_util::{DerivationPath, VariableLengthSeed},
@ -25,7 +24,7 @@ use openpgp::{
stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper},
Parse,
},
policy::{NullPolicy, StandardPolicy, Policy},
policy::{NullPolicy, Policy, StandardPolicy},
serialize::{
stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer},
Marshal,
@ -34,7 +33,6 @@ use openpgp::{
KeyID, PacketPile,
};
pub use sequoia_openpgp as openpgp;
use blahaj::Share;
mod keyring;
use keyring::Keyring;
@ -94,7 +92,7 @@ pub struct EncryptedMessage {
}
impl EncryptedMessage {
/// Create a new EncryptedMessage from known parts.
/// Create a new [`EncryptedMessage`] from known parts.
pub fn new(pkesks: &mut Vec<PKESK>, seip: SEIP) -> Self {
Self {
pkesks: std::mem::take(pkesks),
@ -160,7 +158,7 @@ impl EncryptedMessage {
/// Decrypt the message with a Sequoia policy and decryptor.
///
/// This method creates a container containing the packets and passes the serialized container
/// to a DecryptorBuilder, which is used to decrypt the message.
/// to a `DecryptorBuilder`, which is used to decrypt the message.
///
/// # Errors
/// The method may return an error if it is unable to rebuild the message to decrypt or if it
@ -238,7 +236,7 @@ impl OpenPGP {
let policy = StandardPolicy::new();
let valid_cert = cert.with_policy(&policy, None).map_err(Error::Sequoia)?;
if get_encryption_keys(&valid_cert).next().is_none() {
return Err(Error::NoValidKeys(valid_cert.keyid()))
return Err(Error::NoValidKeys(valid_cert.keyid()));
}
}
Ok(certs.into_values().collect())
@ -265,7 +263,7 @@ impl Format for OpenPGP {
.derive_path(&path)
.expect(bug!("valid derivation"));
keyfork_derive_openpgp::derive(
xprv,
&xprv,
&[KeyFlags::empty().set_certification().set_signing()],
&userid,
)
@ -450,8 +448,8 @@ impl Format for OpenPGP {
// We don't want to invalidate someone's keys just because the old sig expired.
let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone())?;
let mut manager = SmartcardManager::new(prompt.clone())?;
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone());
let mut manager = SmartcardManager::new(prompt.clone());
let mut encrypted_messages = encrypted_data.iter();
@ -482,9 +480,9 @@ impl Format for OpenPGP {
let left_from_threshold = threshold as usize - decrypted_messages.len();
if left_from_threshold > 0 {
#[allow(clippy::cast_possible_truncation)]
let new_messages = decrypt_with_manager(
left_from_threshold as u8,
u8::try_from(left_from_threshold)
.expect(bug!("threshold too large: {}", left_from_threshold)),
&mut messages,
&certs,
&policy,
@ -509,8 +507,8 @@ impl Format for OpenPGP {
) -> std::result::Result<(Share, u8), Self::Error> {
let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone())?;
let mut manager = SmartcardManager::new(prompt.clone())?;
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone());
let mut manager = SmartcardManager::new(prompt.clone());
let mut encrypted_messages = encrypted_data.iter();
@ -557,8 +555,8 @@ impl Format for OpenPGP {
prompt: Rc<Mutex<Box<dyn PromptHandler>>>,
) -> std::result::Result<(u8, Vec<Self::PublicKey>), Self::Error> {
let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone())?;
let mut manager = SmartcardManager::new(prompt.clone())?;
let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone());
let mut manager = SmartcardManager::new(prompt.clone());
let mut encrypted_messages = encrypted_data.iter();
let metadata = encrypted_messages

View File

@ -1,5 +1,3 @@
#![allow(clippy::expect_fun_call)]
use std::{rc::Rc, sync::Mutex};
use keyfork_bug::{bug, POISONED_MUTEX};
@ -25,8 +23,6 @@ pub enum Error {
Prompt(#[from] PromptError),
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
pub struct Keyring {
full_certs: Vec<Cert>,
root: Option<Cert>,
@ -34,12 +30,12 @@ pub struct Keyring {
}
impl Keyring {
pub fn new(certs: impl AsRef<[Cert]>, p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Result<Self> {
Ok(Self {
pub fn new(certs: impl AsRef<[Cert]>, p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Self {
Self {
full_certs: certs.as_ref().to_vec(),
root: Default::default(),
root: Option::default(),
pm: p,
})
}
}
pub fn is_empty(&self) -> bool {

View File

@ -1,5 +1,3 @@
#![allow(clippy::expect_fun_call)]
use std::{
collections::{HashMap, HashSet},
rc::Rc,
@ -81,13 +79,13 @@ pub struct SmartcardManager {
}
impl SmartcardManager {
pub fn new(p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Result<Self> {
Ok(Self {
pub fn new(p: Rc<Mutex<Box<dyn PromptHandler>>>) -> Self {
Self {
current_card: None,
root: None,
pm: p,
pin_cache: Default::default(),
})
pin_cache: HashMap::default(),
}
}
// Sets the root cert, returning the old cert
@ -177,10 +175,9 @@ impl SmartcardManager {
impl VerificationHelper for &mut SmartcardManager {
fn get_certs(&mut self, ids: &[openpgp::KeyHandle]) -> openpgp::Result<Vec<Cert>> {
#[allow(clippy::flat_map_option)]
Ok(ids
.iter()
.flat_map(|kh| self.root.as_ref().filter(|cert| cert.key_handle() == *kh))
.filter_map(|kh| self.root.as_ref().filter(|cert| cert.key_handle() == *kh))
.cloned()
.collect())
}
@ -281,8 +278,7 @@ impl DecryptionHelper for &mut SmartcardManager {
let temp_pin = prompt_validated_passphrase(&mut **prompt, &message, 3, &pin_validator)?;
let verification_status = transaction.verify_user_pin(temp_pin.as_str().trim());
match verification_status {
#[allow(clippy::ignored_unit_patterns)]
Ok(_) => {
Ok(()) => {
self.pin_cache.insert(fp.clone(), temp_pin.clone());
pin.replace(temp_pin);
}

View File

@ -4,6 +4,9 @@ version = "0.3.3"
edition = "2021"
license = "AGPL-3.0-only"
[lints]
workspace = true
[features]
default = [
"completion",
@ -47,6 +50,6 @@ clap_complete = { version = "4.4.6", optional = true }
sequoia-openpgp = { workspace = true }
keyforkd-models.workspace = true
base64.workspace = true
nix = { version = "0.29.0", default-features = false, features = ["process"] }
nix = { version = "0.30.0", default-features = false, features = ["process"] }
shlex = "1.3.0"
tempfile.workspace = true

View File

@ -43,7 +43,7 @@ impl FromStr for Options {
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Ok(Default::default())
return Ok(Self::default());
}
let values = s
.split(',')

View File

@ -1,4 +1,4 @@
use super::{Keyfork, create};
use super::{create, Keyfork};
use clap::{Args, Parser, Subcommand, ValueEnum};
use std::{fmt::Display, io::Write, path::PathBuf};
@ -26,9 +26,9 @@ pub trait Deriver {
fn derivation_path(&self) -> DerivationPath;
fn derive_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()>;
fn derive_with_xprv(&self, writer: OptWrite, xprv: &XPrv<Self::Prv>) -> Result<()>;
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()>;
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: &XPrv<Self::Prv>) -> Result<()>;
}
#[derive(Subcommand, Clone, Debug)]
@ -138,12 +138,12 @@ impl std::str::FromStr for Slug {
impl Display for Slug {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
#[allow(clippy::redundant_at_rest_pattern)]
match (self.0.inner() & (0b1 << 31)).to_be_bytes().as_slice() {
[0, 0, 0, 0] => Ok(()),
[0, 0, 0, bytes @ ..] | [0, 0, bytes @ ..] | [0, bytes @ ..] | [bytes @ ..] => f
.write_str(
std::str::from_utf8(&bytes[..]).expect("slug constructed from non-utf8"),
),
[0, 0, 0, bytes @ ..] | [0, 0, bytes @ ..] | [0, bytes @ ..] | [bytes @ ..] => {
f.write_str(std::str::from_utf8(bytes).expect("slug constructed from non-utf8"))
}
}
}
}
@ -170,9 +170,9 @@ impl DeriveSubcommands {
let xprv = Client::discover_socket()?
.request_xprv::<<OpenPGP as Deriver>::Prv>(&path.chain_push(account))?;
if is_public {
opgp.derive_public_with_xprv(writer, xprv)
opgp.derive_public_with_xprv(writer, &xprv)
} else {
opgp.derive_with_xprv(writer, xprv)
opgp.derive_with_xprv(writer, &xprv)
}
}
DeriveSubcommands::Key(key) => {
@ -180,9 +180,9 @@ impl DeriveSubcommands {
let xprv = Client::discover_socket()?
.request_xprv::<<Key as Deriver>::Prv>(&path.chain_push(account))?;
if is_public {
key.derive_public_with_xprv(writer, xprv)
key.derive_public_with_xprv(writer, &xprv)
} else {
key.derive_with_xprv(writer, xprv)
key.derive_with_xprv(writer, &xprv)
}
}
}
@ -190,7 +190,7 @@ impl DeriveSubcommands {
}
impl OpenPGP {
fn cert_from_xprv(&self, xprv: keyfork_derive_openpgp::XPrv) -> Result<Cert> {
fn cert_from_xprv(&self, xprv: &keyfork_derive_openpgp::XPrv) -> Result<Cert> {
let subkeys = vec![
KeyFlags::empty().set_certification(),
KeyFlags::empty().set_signing(),
@ -213,15 +213,12 @@ impl Deriver for OpenPGP {
self.derivation_path.derivation_path()
}
fn derive_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
fn derive_with_xprv(&self, writer: OptWrite, xprv: &XPrv<Self::Prv>) -> Result<()> {
let cert = self.cert_from_xprv(xprv)?;
let writer = match writer {
Some(w) => w,
None => {
let writer = if let Some(writer) = writer { writer } else {
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
let file = create(&path)?;
Box::new(file)
}
};
let mut writer = Writer::new(writer, Kind::SecretKey)?;
for packet in cert.as_tsk().into_packets() {
@ -231,15 +228,12 @@ impl Deriver for OpenPGP {
Ok(())
}
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: &XPrv<Self::Prv>) -> Result<()> {
let cert = self.cert_from_xprv(xprv)?;
let writer = match writer {
Some(w) => w,
None => {
let writer = if let Some(writer) = writer { writer } else {
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
let file = create(&path)?;
Box::new(file)
}
};
let mut writer = Writer::new(writer, Kind::PublicKey)?;
for packet in cert.into_packets2() {
@ -259,7 +253,7 @@ impl Deriver for Key {
DerivationPath::default().chain_push(self.slug.0.clone())
}
fn derive_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
fn derive_with_xprv(&self, writer: OptWrite, xprv: &XPrv<Self::Prv>) -> Result<()> {
let (formatted, ext) = match self.format {
KeyFormat::Hex => (smex::encode(xprv.private_key().to_bytes()), "hex"),
KeyFormat::Base64 => {
@ -278,7 +272,7 @@ impl Deriver for Key {
Ok(())
}
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: XPrv<Self::Prv>) -> Result<()> {
fn derive_public_with_xprv(&self, writer: OptWrite, xprv: &XPrv<Self::Prv>) -> Result<()> {
let (formatted, ext) = match self.format {
KeyFormat::Hex => (smex::encode(xprv.public_key().to_bytes()), "hex"),
KeyFormat::Base64 => {

View File

@ -1,14 +1,13 @@
use super::{
create,
derive::{self, Deriver},
provision,
Keyfork,
provision, Keyfork,
};
use crate::{clap_ext::*, config, openpgp_card::factory_reset_current_card};
use card_backend_pcsc::PcscBackend;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
fmt::Display,
fs::File,
io::{IsTerminal, Write},
@ -20,7 +19,7 @@ use keyfork_derive_openpgp::{
openpgp::{
self,
armor::{Kind, Writer},
packet::{UserID, signature::SignatureBuilder},
packet::{signature::SignatureBuilder, UserID},
policy::StandardPolicy,
serialize::{
stream::{Encryptor2, LiteralWriter, Message, Recipient},
@ -165,7 +164,7 @@ pub enum Error {
MissingOption(&'static str),
}
fn context_stub<'a>(path: &'a Path) -> impl Fn(std::io::Error) -> Error + 'a {
fn context_stub(path: &Path) -> impl Fn(std::io::Error) -> Error + use<'_> {
|e| Error::IOContext(e, path.to_path_buf())
}
@ -228,13 +227,13 @@ pub enum MnemonicSubcommands {
///
/// The following additional arguments are available:
///
/// * threshold, m: the minimum amount of shares required to reconstitute the shard. By
/// * `threshold`, m: the minimum amount of shares required to reconstitute the shard. By
/// default, this is the amount of certificates provided.
///
/// * max, n: the maximum amount of shares. When provided, this is used to ensure the
/// * `max`, n: the maximum amount of shares. When provided, this is used to ensure the
/// certificate count is correct. This is required when using `threshold` or `m`.
///
/// * output: the file to write the generated Shardfile to. By default, assuming the
/// * `output`: the file to write the generated Shardfile to. By default, assuming the
/// certificate input is `input.asc`, the generated Shardfile would be written to
/// `input.shard.asc`.
#[arg(long)]
@ -257,13 +256,13 @@ pub enum MnemonicSubcommands {
///
/// The following additional arguments are required:
///
/// * threshold, m: the minimum amount of shares required to reconstitute the shard.
/// * `threshold`, m: the minimum amount of shares required to reconstitute the shard.
///
/// * max, n: the maximum amount of shares.
/// * `max`, n: the maximum amount of shares.
///
/// * cards_per_shard: the amount of OpenPGP smartcards to provision per shardholder.
/// * `cards_per_shard`: the amount of OpenPGP smartcards to provision per shardholder.
///
/// * cert_output: the file to write all generated OpenPGP certificates to; if not
/// * `cert_output`: the file to write all generated OpenPGP certificates to; if not
/// provided, files will be automatically generated for each certificate.
#[arg(long)]
shard_to_self: Option<ValueWithOptions<PathBuf>>,
@ -303,24 +302,23 @@ fn determine_valid_output_path<T: AsRef<Path>>(
mid_ext: &str,
optional_path: Option<T>,
) -> PathBuf {
match optional_path {
Some(p) => p.as_ref().to_path_buf(),
None => {
if let Some(p) = optional_path {
p.as_ref().to_path_buf()
} else {
let extension = match path.extension() {
Some(ext) => format!("{mid_ext}.{ext}", ext = ext.to_string_lossy()),
None => format!("{mid_ext}.asc"),
};
path.with_extension(extension)
}
}
}
fn is_extension_armored(path: &Path) -> bool {
match path.extension().and_then(|s| s.to_str()) {
Some("pgp") | Some("gpg") => false,
Some("pgp" | "gpg") => false,
Some("asc") => true,
_ => {
eprintln!("unable to determine whether to armor file: {path:?}");
eprintln!("unable to determine whether to armor file: {path}", path = path.display());
eprintln!("use .gpg, .pgp, or .asc extension, or `armor=true`");
eprintln!("defaulting to armored");
true
@ -395,8 +393,11 @@ fn do_encrypt_to_self(
.clone()
.chain_push(account);
let cert =
keyfork_derive_openpgp::derive(xprv.derive_path(&derivation_path)?, &subkeys, &userid)?;
let cert = keyfork_derive_openpgp::derive(
&xprv.derive_path(&derivation_path)?,
&subkeys,
&userid,
)?;
certs.push(cert);
}
@ -456,15 +457,12 @@ fn do_shard(
// if only one is set: true
if threshold.is_some() ^ max.is_some() {
return Err(MissingThresholdOrMax)?;
return Err(MissingThresholdOrMax.into());
}
let (threshold, max) = match threshold.zip(max) {
Some(t) => t,
None => {
let (threshold, max) = if let Some(t) = threshold.zip(max) { t } else {
let len = u8::try_from(certs.len())?;
(len, len)
}
};
let openpgp = keyfork_shard::openpgp::OpenPGP;
@ -544,12 +542,14 @@ fn derive_key(seed: [u8; 64], index: u8) -> Result<openpgp::Cert, Box<dyn std::e
];
let subkey = DerivationIndex::new(u32::from(index), true)?;
let path = keyfork_derive_path_data::paths::OPENPGP_SHARD.clone().chain_push(subkey);
let path = keyfork_derive_path_data::paths::OPENPGP_SHARD
.clone()
.chain_push(subkey);
let xprv = XPrv::new(seed)
.expect("could not construct master key from seed")
.derive_path(&path)?;
let userid = UserID::from(format!("Keyfork Shard {index}"));
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
let cert = keyfork_derive_openpgp::derive(&xprv, &subkeys, &userid)?;
Ok(cert)
}
@ -611,7 +611,6 @@ fn do_shard_to_self(
.parse()?;
let cards_per_shard = options
.get("cards_per_shard")
.as_deref()
.map(|cps| u8::from_str(cps))
.transpose()?;
@ -626,8 +625,8 @@ fn do_shard_to_self(
for i in 0..cards_per_shard.unwrap_or(1) {
pm.prompt_message(keyfork_prompt::Message::Text(format!(
"Please remove all keys and insert key #{} for user #{}",
(i as u16) + 1,
(index as u16) + 1,
(u16::from(i)) + 1,
(u16::from(index)) + 1,
)))?;
let card_backend = loop {
if let Some(c) = PcscBackend::cards(None)?.next().transpose()? {
@ -671,7 +670,7 @@ fn do_shard_to_self(
let output = File::create(path)?;
opgp.shard_and_encrypt(
threshold,
certs.len() as u8,
u8::try_from(certs.len()).expect("provided more than u8::MAX certs"),
mnemonic.as_bytes(),
&certs[..],
output,
@ -742,10 +741,10 @@ fn do_provision(
metadata @ None => {
*metadata = Some(config.clone());
}
};
}
provision
.provisioner_name
.provision_with_mnemonic(mnemonic, provisioner)?;
.provision_with_mnemonic(mnemonic, &provisioner)?;
}
Ok(())
@ -772,11 +771,11 @@ fn do_derive(
use keyfork_derive_openpgp::XPrv;
let root_xprv = XPrv::new(mnemonic.generate_seed(None))?;
let account = DerivationIndex::new(*account_id, true)?;
let derived = root_xprv.derive_path(&opgp.derivation_path().chain_push(account))?;
let derived_key = root_xprv.derive_path(&opgp.derivation_path().chain_push(account))?;
if *public {
opgp.derive_public_with_xprv(writer, derived)?;
opgp.derive_public_with_xprv(writer, &derived_key)?;
} else {
opgp.derive_with_xprv(writer, derived)?;
opgp.derive_with_xprv(writer, &derived_key)?;
}
}
derive::Derive {
@ -790,11 +789,11 @@ fn do_derive(
use keyfork_derive_openpgp::XPrv;
let root_xprv = XPrv::new(mnemonic.generate_seed(None))?;
let account = DerivationIndex::new(*account_id, true)?;
let derived = root_xprv.derive_path(&key.derivation_path().chain_push(account))?;
let derived_key = root_xprv.derive_path(&key.derivation_path().chain_push(account))?;
if *public {
key.derive_public_with_xprv(writer, derived)?;
key.derive_public_with_xprv(writer, &derived_key)?;
} else {
key.derive_with_xprv(writer, derived)?;
key.derive_with_xprv(writer, &derived_key)?;
}
}
}
@ -802,6 +801,7 @@ fn do_derive(
}
impl MnemonicSubcommands {
#[allow(clippy::too_many_lines)]
pub fn handle(
&self,
_m: &Mnemonic,
@ -831,13 +831,13 @@ impl MnemonicSubcommands {
// * Sharding to existing, usable keys
// * Sharding to newly provisioned keys
let mut will_print_mnemonic =
encrypt_to.is_none() || encrypt_to.as_ref().is_some_and(|e| e.is_empty());
encrypt_to.is_none() || encrypt_to.as_ref().is_some_and(Vec::is_empty);
will_print_mnemonic = will_print_mnemonic
&& (encrypt_to_self.as_ref().is_none() || provision.as_ref().is_none());
will_print_mnemonic = will_print_mnemonic && shard_to.is_none()
|| shard_to.as_ref().is_some_and(|s| s.is_empty());
|| shard_to.as_ref().is_some_and(Vec::is_empty);
will_print_mnemonic = will_print_mnemonic && shard.is_none()
|| shard.as_ref().is_some_and(|s| s.is_empty());
|| shard.as_ref().is_some_and(Vec::is_empty);
will_print_mnemonic = will_print_mnemonic && shard_to_self.is_none();
let mnemonic = source.handle(size)?;
@ -859,7 +859,7 @@ impl MnemonicSubcommands {
}
if let Some(encrypt_to_self) = encrypt_to_self {
let mut accounts: std::collections::HashSet<u32> = Default::default();
let mut accounts: HashSet<u32> = HashSet::default();
if let Some(provision::Provision {
provisioner_name: provision::Provisioner::OpenPGPCard(_),
account_id,
@ -884,7 +884,7 @@ impl MnemonicSubcommands {
!indices.is_empty(),
"neither derived nor provisioned accounts were found"
);
do_encrypt_to_self(&mnemonic, &encrypt_to_self, &indices)?;
do_encrypt_to_self(&mnemonic, encrypt_to_self, &indices)?;
}
if let Some(shard_to_self) = shard_to_self {
@ -938,7 +938,7 @@ impl MnemonicSubcommands {
}
if will_print_mnemonic {
println!("{}", mnemonic);
println!("{mnemonic}");
}
Ok(())
}

View File

@ -7,7 +7,7 @@ mod recover;
mod shard;
pub fn create(path: &std::path::Path) -> std::io::Result<std::fs::File> {
eprintln!("Writing derived key to: {path}", path=path.display());
eprintln!("Writing derived key to: {path}", path = path.display());
std::fs::File::create(path)
}
@ -20,6 +20,7 @@ pub struct Keyfork {
pub command: KeyforkCommands,
}
#[allow(clippy::large_enum_variant)]
#[derive(Subcommand, Clone, Debug)]
pub enum KeyforkCommands {
/// Derive keys of various formats. These commands require that the Keyfork server is running,
@ -95,12 +96,7 @@ impl KeyforkCommands {
KeyforkCommands::Completion { shell } => {
let mut command = Keyfork::command();
let command_name = command.get_name().to_string();
clap_complete::generate(
*shell,
&mut command,
command_name,
&mut std::io::stdout(),
);
clap_complete::generate(*shell, &mut command, command_name, &mut std::io::stdout());
}
}
Ok(())

View File

@ -38,7 +38,7 @@ impl Provisioner {
pub fn provision(
&self,
provisioner: config::Provisioner,
provisioner: &config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> {
match self {
Provisioner::OpenPGPCard(o) => {
@ -49,7 +49,7 @@ impl Provisioner {
.chain_push(account_index);
let mut client = keyforkd_client::Client::discover_socket()?;
let xprv: XPrv = client.request_xprv(&path)?;
o.provision(xprv, provisioner)
o.provision(&xprv, provisioner)
}
Provisioner::Shard(s) => {
type Prv = <openpgp::Shard as ProvisionExec>::PrivateKey;
@ -59,7 +59,7 @@ impl Provisioner {
.chain_push(account_index);
let mut client = keyforkd_client::Client::discover_socket()?;
let xprv: XPrv = client.request_xprv(&path)?;
s.provision(xprv, provisioner)
s.provision(&xprv, provisioner)
}
}
}
@ -67,7 +67,7 @@ impl Provisioner {
pub fn provision_with_mnemonic(
&self,
mnemonic: &keyfork_mnemonic::Mnemonic,
provisioner: config::Provisioner,
provisioner: &config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> {
match self {
Provisioner::OpenPGPCard(o) => {
@ -77,7 +77,7 @@ impl Provisioner {
let path = <openpgp::OpenPGPCard as ProvisionExec>::derivation_prefix()
.chain_push(account_index);
let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?;
o.provision(xprv, provisioner)
o.provision(&xprv, provisioner)
}
Provisioner::Shard(s) => {
type Prv = <openpgp::Shard as ProvisionExec>::PrivateKey;
@ -86,7 +86,7 @@ impl Provisioner {
let path = <openpgp::Shard as ProvisionExec>::derivation_prefix()
.chain_push(account_index);
let xprv = XPrv::new(mnemonic.generate_seed(None))?.derive_path(&path)?;
s.provision(xprv, provisioner)
s.provision(&xprv, provisioner)
}
}
}
@ -94,7 +94,10 @@ impl Provisioner {
impl ValueEnum for Provisioner {
fn value_variants<'a>() -> &'a [Self] {
&[Self::OpenPGPCard(openpgp::OpenPGPCard), Self::Shard(openpgp::Shard)]
&[
Self::OpenPGPCard(openpgp::OpenPGPCard),
Self::Shard(openpgp::Shard),
]
}
fn to_possible_value(&self) -> Option<PossibleValue> {
@ -129,8 +132,8 @@ trait ProvisionExec {
/// Derive a key and deploy it to a target.
fn provision(
&self,
xprv: keyfork_derive_util::ExtendedPrivateKey<Self::PrivateKey>,
p: config::Provisioner,
xprv: &keyfork_derive_util::ExtendedPrivateKey<Self::PrivateKey>,
p: &config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>>;
}
@ -186,7 +189,7 @@ impl TryFrom<Provision> for config::Provisioner {
Ok(Self {
account: value.account_id,
identifier: value.identifier.ok_or(MissingField("identifier"))?,
metadata: Default::default(),
metadata: Option::default(),
})
}
}
@ -207,9 +210,9 @@ impl Provision {
}
}
None => {
let provisioner_with_identifier = match self.identifier {
Some(_) => self.clone(),
None => {
let provisioner_with_identifier = if self.identifier.is_some() {
self.clone()
} else {
let identifiers = self.provisioner_name.discover()?;
let [id] = &identifiers[..] else {
panic!("invalid amount of identifiers; pass --identifier");
@ -218,10 +221,9 @@ impl Provision {
identifier: Some(id.0.clone()),
..self.clone()
}
}
};
let config = config::Provisioner::try_from(provisioner_with_identifier)?;
self.provisioner_name.provision(config)?;
self.provisioner_name.provision(&config)?;
}
}
Ok(())

View File

@ -22,7 +22,9 @@ use std::path::PathBuf;
#[error("Provisioner was unable to find a matching smartcard")]
struct NoMatchingSmartcard;
fn discover_cards() -> Result<Vec<(String, Option<String>)>, Box<dyn std::error::Error>> {
pub type CardList = Vec<(String, Option<String>)>;
fn discover_cards() -> Result<CardList, Box<dyn std::error::Error>> {
let mut idents = vec![];
for backend in PcscBackend::cards(None)? {
let backend = backend?;
@ -37,8 +39,8 @@ fn discover_cards() -> Result<Vec<(String, Option<String>)>, Box<dyn std::error:
}
fn provision_card(
provisioner: config::Provisioner,
xprv: XPrv,
provisioner: &config::Provisioner,
xprv: &XPrv,
) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = default_handler()?;
@ -57,23 +59,24 @@ fn provision_card(
Some(userid) => UserID::from(userid.as_str()),
None => UserID::from("Keyfork-Provisioned Key"),
};
let cert = keyfork_derive_openpgp::derive(xprv.clone(), &subkeys, &userid)?;
let cert = keyfork_derive_openpgp::derive(xprv, &subkeys, &userid)?;
if !provisioner
.metadata
.as_ref()
.is_some_and(|m| m.contains_key("_skip_cert_output"))
{
let cert_output = match provisioner.metadata.as_ref().and_then(|m| m.get("output")) {
Some(cert_output) => PathBuf::from(cert_output),
None => {
let cert_output = if let Some(cert_output) =
provisioner.metadata.as_ref().and_then(|m| m.get("output"))
{
PathBuf::from(cert_output)
} else {
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
eprintln!(
"Writing OpenPGP certificate to: {path}",
path = path.display()
);
path
}
};
let cert_output_file = std::fs::File::create(cert_output)?;
@ -100,7 +103,7 @@ fn provision_card(
}
if !has_provisioned {
return Err(NoMatchingSmartcard)?;
return Err(NoMatchingSmartcard.into());
}
Ok(())
@ -122,8 +125,8 @@ impl ProvisionExec for OpenPGPCard {
fn provision(
&self,
xprv: XPrv,
provisioner: config::Provisioner,
xprv: &XPrv,
provisioner: &config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> {
provision_card(provisioner, xprv)
}
@ -145,8 +148,8 @@ impl ProvisionExec for Shard {
fn provision(
&self,
xprv: XPrv,
provisioner: config::Provisioner,
xprv: &XPrv,
provisioner: &config::Provisioner,
) -> Result<(), Box<dyn std::error::Error>> {
provision_card(provisioner, xprv)
}

View File

@ -1,10 +1,10 @@
use super::Keyfork;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use nix::{
sys::wait::waitpid,
unistd::{fork, ForkResult},
};
use std::path::PathBuf;
use keyfork_mnemonic::{English, Mnemonic};
use keyfork_prompt::{
@ -86,7 +86,7 @@ pub struct Recover {
command: RecoverSubcommands,
/// Daemonize the server once started, restoring control back to the shell.
#[arg(long, global=true)]
#[arg(long, global = true)]
daemon: bool,
}
@ -102,12 +102,12 @@ impl Recover {
// wait for the child to die, so we don't exit prematurely
waitpid(Some(child), None)?;
return Ok(());
},
}
ForkResult::Child => {
if let ForkResult::Parent { .. } = unsafe { fork() }? {
return Ok(());
}
},
}
}
}
tokio::runtime::Builder::new_multi_thread()

View File

@ -109,14 +109,15 @@ impl ShardExec for OpenPGP {
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>> {
use keyfork_derive_openpgp::openpgp::{
armor::{Kind, Writer},
serialize::Marshal,
armor::{Writer, Kind},
};
let openpgp = keyfork_shard::openpgp::OpenPGP;
let prompt = default_handler()?;
let (threshold, certs) = openpgp.decrypt_metadata_from_file(key_discovery, input, prompt)?;
let (threshold, certs) =
openpgp.decrypt_metadata_from_file(key_discovery, input, prompt)?;
let mut writer = Writer::new(output_pubkeys, Kind::PublicKey)?;
for cert in certs {
cert.serialize(&mut writer)?;
@ -187,7 +188,7 @@ pub enum ShardSubcommands {
/// The path to discover private keys from.
key_discovery: Option<PathBuf>,
}
},
}
impl ShardSubcommands {
@ -256,7 +257,11 @@ impl ShardSubcommands {
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
}
}
ShardSubcommands::Metadata { shardfile, output_pubkeys, key_discovery } => {
ShardSubcommands::Metadata {
shardfile,
output_pubkeys,
key_discovery,
} => {
let shard_content = std::fs::read_to_string(shardfile)?;
if shard_content.contains("BEGIN PGP MESSAGE") {
let _ = format.insert(Format::OpenPGP(OpenPGP));

View File

@ -1,5 +1,4 @@
#![doc = include_str!("../README.md")]
#![allow(clippy::module_name_repetitions)]
use std::process::ExitCode;
@ -8,9 +7,9 @@ use clap::Parser;
use keyfork_bin::{Bin, ClosureBin};
pub mod clap_ext;
mod cli;
mod config;
pub mod clap_ext;
mod openpgp_card;
fn main() -> ExitCode {

View File

@ -34,11 +34,10 @@ pub fn get_new_pins(
3,
&user_pin_validator,
)?;
if user_pin != validated_user_pin {
pm.prompt_message(Message::Text("User PINs did not match. Retrying.".into()))?;
} else {
if user_pin == validated_user_pin {
break user_pin;
}
pm.prompt_message(Message::Text("User PINs did not match. Retrying.".into()))?;
};
let admin_pin = loop {
@ -54,11 +53,10 @@ pub fn get_new_pins(
3,
&admin_pin_validator,
)?;
if admin_pin != validated_admin_pin {
pm.prompt_message(Message::Text("Admin PINs did not match. Retrying.".into()))?;
} else {
if admin_pin == validated_admin_pin {
break admin_pin;
}
pm.prompt_message(Message::Text("Admin PINs did not match. Retrying.".into()))?;
};
Ok((user_pin, admin_pin))

View File

@ -5,6 +5,9 @@ repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]

View File

@ -2,10 +2,12 @@
use keyfork_bug as bug;
use bug::POISONED_MUTEX;
use image::{ImageBuffer, ImageReader, Luma};
use std::{
io::{Cursor, Write},
process::{Command, Stdio},
sync::{mpsc::channel, Arc, Condvar, Mutex},
time::{Duration, Instant},
};
use v4l::{
@ -15,6 +17,8 @@ use v4l::{
Device, FourCC,
};
type Image = ImageBuffer<Luma<u8>, Vec<u8>>;
/// A QR code could not be generated.
#[derive(thiserror::Error, Debug)]
pub enum QRGenerationError {
@ -33,10 +37,10 @@ pub enum QRCodeScanError {
/// The camera could not load the requested format.
#[error("Camera could not use {expected} format, instead used {actual}")]
CameraGaveBadFormat {
/// The expected format, in FourCC format.
/// The expected format, in `FourCC` format.
expected: String,
/// The actual format, in FourCC format.
/// The actual format, in `FourCC` format.
actual: String,
},
@ -102,12 +106,12 @@ pub fn qrencode(
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
trait Scanner {
fn scan_image(&mut self, image: ImageBuffer<Luma<u8>, Vec<u8>>) -> Option<String>;
fn scan_image(&mut self, image: Image) -> Option<String>;
}
#[cfg(feature = "decode-backend-zbar")]
mod zbar {
use super::{ImageBuffer, Luma, Scanner};
use super::{Image, Scanner};
pub struct Zbar {
scanner: keyfork_zbar::image_scanner::ImageScanner,
@ -129,29 +133,25 @@ mod zbar {
}
impl Scanner for Zbar {
fn scan_image(
&mut self,
image: ImageBuffer<Luma<u8>, Vec<u8>>,
) -> Option<String> {
fn scan_image(&mut self, image: Image) -> Option<String> {
let image = keyfork_zbar::image::Image::from(image);
self.scanner.scan_image(&image).into_iter().next().map(|symbol| {
String::from_utf8_lossy(symbol.data()).into()
})
self.scanner
.scan_image(&image)
.into_iter()
.next()
.map(|symbol| String::from_utf8_lossy(symbol.data()).into())
}
}
}
#[cfg(feature = "decode-backend-rqrr")]
mod rqrr {
use super::{ImageBuffer, Luma, Scanner};
use super::{Image, Scanner};
pub struct Rqrr;
impl Scanner for Rqrr {
fn scan_image(
&mut self,
image: ImageBuffer<Luma<u8>, Vec<u8>>,
) -> Option<String> {
fn scan_image(&mut self, image: Image) -> Option<String> {
let mut image = rqrr::PreparedImage::prepare(image);
for grid in image.detect_grids() {
if let Ok((_, content)) = grid.decode() {
@ -163,11 +163,18 @@ mod rqrr {
}
}
#[allow(dead_code)]
#[allow(dead_code, clippy::cast_precision_loss)]
fn dbg_elapsed(count: u64, instant: Instant) {
let elapsed = instant.elapsed().as_secs();
let framerate = count as f64 / elapsed as f64;
eprintln!("framerate: {count}/{elapsed} = {framerate}");
std::thread::sleep(std::time::Duration::from_secs(5));
}
#[derive(Debug)]
struct ScanQueue {
shutdown: bool,
images: Vec<Image>,
}
/// Continuously scan the `index`-th camera for a QR code.
@ -176,6 +183,11 @@ fn dbg_elapsed(count: u64, instant: Instant) {
///
/// The function may return an error if the hardware is unable to scan video or if an image could
/// not be decoded.
///
/// # Panics
///
/// The function may panic if a mutex is poisoned by a thread panicking, which should
/// only happen during a mutex, or if it can't send a message over the mpsc channel.
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
let device = Device::new(index)?;
let mut fmt = device
@ -185,6 +197,26 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
device.set_format(&fmt)?;
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
let start = Instant::now();
#[allow(unused)]
let mut count = 0;
let thread_count = 4;
std::thread::scope(|scope| {
let scan_queue = ScanQueue {
shutdown: false,
images: vec![],
};
let arced = Arc::new((Mutex::new(scan_queue), Condvar::new()));
let (tx, rx) = channel();
for _ in 0..thread_count {
let tx = tx.clone();
let arced = arced.clone();
scope.spawn(move || {
cfg_if::cfg_if! {
if #[cfg(feature = "decode-backend-zbar")] {
let mut scanner = zbar::Zbar::default();
@ -195,23 +227,66 @@ pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QR
}
};
#[allow(unused)]
let mut count = 0;
let (queue_mutex, condvar) = &*arced;
loop {
// NOTE: Carrying the `queue` variable through the loop, so we can
// pass it through without re-locking, means that we don't drop the
// lock on the mutex. Therefore, we unlock, then immediately
// re-lock when we pass the value to wait_while().
//
// By holding onto the queue until we pass it back to the Condvar,
// and checking shutdown, we ensure that there's no way we miss the
// shutdown being set before we release the guard on the queue.
let queue = queue_mutex.lock().expect(bug::bug!(POISONED_MUTEX));
if queue.shutdown {
break;
}
let mut queue = condvar
.wait_while(queue, |queue| {
queue.images.is_empty() && !queue.shutdown
})
.expect(bug::bug!(POISONED_MUTEX));
if let Some(image) = queue.images.pop() {
// drop the queue here since this is what's going to take time
drop(queue);
if let Some(content) = scanner.scan_image(image) {
if tx.send(content).is_err() {
break;
}
}
}
}
});
}
while Instant::now().duration_since(start) < timeout {
if let Ok(content) = rx.try_recv() {
arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true;
arced.1.notify_all();
return Ok(Some(content));
}
count += 1;
let (buffer, _) = stream.next()?;
let image = ImageReader::new(Cursor::new(buffer))
.with_guessed_format()?
.decode()?
.to_luma8();
if let Some(content) = scanner.scan_image(image) {
// dbg_elapsed(count, start);
return Ok(Some(content));
}
arced
.0
.lock()
.expect(bug::bug!(POISONED_MUTEX))
.images
.push(image);
arced.1.notify_one();
}
// dbg_elapsed(count, start);
arced.0.lock().expect(bug::bug!(POISONED_MUTEX)).shutdown = true;
arced.1.notify_all();
Ok(None)
})
}

View File

@ -5,10 +5,13 @@ repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[build-dependencies]
bindgen = { version = "0.68", default-features = false, features = ["runtime"] }
bindgen = { version = "0.70", default-features = false, features = ["runtime"] }
pkg-config = "0.3"

View File

@ -45,7 +45,7 @@ fn main() -> Result<()> {
if let Err(e) = generate_bindings_file() {
eprintln!("Building zbar-sys failed: {e}");
eprintln!("Ensure zbar headers, libclang, and pkg-config are installed");
return Err(e)
return Err(e);
}
Ok(())

View File

@ -1,4 +1,5 @@
#![allow(non_upper_case_globals, non_camel_case_types, non_snake_case)]
#![allow(missing_docs)]
#![allow(clippy::unreadable_literal, clippy::pub_underscore_fields)]
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

View File

@ -5,6 +5,9 @@ repository = "https://git.distrust.co/public/keyfork"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]

View File

@ -7,7 +7,7 @@ use std::{
use keyfork_zbar::{image::Image, image_scanner::ImageScanner};
use image::io::Reader as ImageReader;
use image::ImageReader;
use v4l::{
buffer::Type,
io::{traits::CaptureStream, userptr::Stream},

View File

@ -20,16 +20,13 @@ impl Image {
/// Link: [`sys::zbar_image_set_format`]
///
/// A FourCC code can be given in the format:
/// A `FourCC` code can be given in the format:
///
/// ```rust,ignore
/// self.set_format(b"Y800")
/// ```
pub(crate) fn set_format(&mut self, fourcc: &[u8; 4]) {
let fourcc: u64 = fourcc[0] as u64
| ((fourcc[1] as u64) << 8)
| ((fourcc[2] as u64) << 16)
| ((fourcc[3] as u64) << 24);
let fourcc = std::os::raw::c_ulong::from(u32::from_le_bytes(*fourcc));
unsafe { sys::zbar_image_set_format(self.inner, fourcc) }
}
@ -43,12 +40,7 @@ impl Image {
/// Accepts raw data in the configured format. See: [`Image::set_format`]
fn set_data(&mut self, data: Vec<u8>) {
unsafe {
sys::zbar_image_set_data(
self.inner,
data.as_ptr().cast(),
data.len() as u64,
None,
)
sys::zbar_image_set_data(self.inner, data.as_ptr().cast(), data.len() as u64, None);
}
// keep data in self to avoid use after free when data goes out of scope
let _ = self.inner_data.insert(data);

View File

@ -22,7 +22,7 @@ pub struct ImageScanner {
}
impl ImageScanner {
/// create a new ImageScanner.
/// Create a new `ImageScanner`.
///
/// Link: [`sys::zbar_image_scanner_create`]
pub fn new() -> Self {
@ -31,7 +31,7 @@ impl ImageScanner {
}
}
/// Set a configuration option for the ImageScanner.
/// Set a configuration option.
///
/// Link: [`sys::zbar_image_scanner_set_config`]
///
@ -58,10 +58,7 @@ impl ImageScanner {
/// Link: [`sys::zbar_scan_image`]
///
/// TODO: return an iterator over scanned values
pub fn scan_image(
&mut self,
image: &Image,
) -> Vec<Symbol> {
pub fn scan_image(&mut self, image: &Image) -> Vec<Symbol> {
unsafe { sys::zbar_scan_image(self.inner, image.inner) };
let mut result = vec![];
let mut symbol = unsafe { sys::zbar_image_first_symbol(image.inner) };
@ -70,7 +67,7 @@ impl ImageScanner {
let symbol_data = unsafe { sys::zbar_symbol_get_data(symbol) };
let symbol_data_len = unsafe { sys::zbar_symbol_get_data_length(symbol) };
let symbol_slice = unsafe {
std::slice::from_raw_parts(symbol_data as *const u8, symbol_data_len as usize)
std::slice::from_raw_parts(symbol_data.cast::<u8>(), symbol_data_len as usize)
};
result.push(Symbol::new(symbol_type, symbol_slice));
symbol = unsafe { sys::zbar_symbol_next(symbol) };

View File

@ -11,6 +11,6 @@ pub use sys::zbar_config_e as Config;
pub use sys::zbar_modifier_e as Modifier;
pub use sys::zbar_orientation_e as Orientation;
pub mod image_scanner;
pub mod image;
pub mod image_scanner;
pub mod symbol;

View File

@ -1,5 +1,7 @@
//! A Symbol represents some form of encoded data.
#![allow(clippy::used_underscore_binding)]
use super::sys;
/// The type of symbol (i.e. what type of barcode or QR code).

View File

@ -5,6 +5,9 @@ edition = "2021"
publish = false
license = "MIT"
[lints]
workspace = true
[dependencies]
assert_cmd = "2.0.16"
keyforkd = { workspace = true, features = ["default"] }

View File

@ -52,7 +52,5 @@ fn test() {
key.alive().expect("is live after being generated");
key.parts_into_secret().expect("has secret keys");
}
if !key_formats.is_empty() {
panic!("remaining key formats: {key_formats:?}");
}
assert!(key_formats.is_empty(), "remaining key formats: {key_formats:?}");
}

View File

@ -1,2 +1,4 @@
#![allow(missing_docs)]
#[cfg(test)]
mod keyfork;

View File

@ -4,6 +4,9 @@ version = "0.1.0"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -45,6 +45,7 @@ use std::process::ExitCode;
/// A result that may contain any error.
pub type ProcessResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
#[allow(clippy::needless_pass_by_value)]
fn report_err(e: Box<dyn std::error::Error>) {
eprintln!("Unable to run command: {e}");
let mut source = e.source();
@ -102,10 +103,13 @@ pub trait Bin {
/// A Bin that doesn't take any arguments.
pub struct ClosureBin<F: Fn() -> ProcessResult> {
closure: F
closure: F,
}
impl<F> ClosureBin<F> where F: Fn() -> ProcessResult {
impl<F> ClosureBin<F>
where
F: Fn() -> ProcessResult,
{
/// Create a new Bin from a closure.
///
/// # Examples
@ -120,13 +124,14 @@ impl<F> ClosureBin<F> where F: Fn() -> ProcessResult {
/// bin.main();
/// ```
pub fn new(closure: F) -> Self {
Self {
closure
}
Self { closure }
}
}
impl<F> Bin for ClosureBin<F> where F: Fn() -> ProcessResult {
impl<F> Bin for ClosureBin<F>
where
F: Fn() -> ProcessResult,
{
type Args = ();
fn validate_args(&self, _args: impl Iterator<Item = String>) -> ProcessResult<Self::Args> {

View File

@ -4,6 +4,9 @@ version = "0.1.1"
edition = "2021"
license = "MIT"
[lints]
workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View File

@ -0,0 +1,13 @@
[package]
name = "keyfork-crossterm-ioctl-shim"
version = "0.1.0"
description = "A shim for working with crossterm ioctls with custom fd's"
repository = "https://git.distrust.co/public/keyfork"
edition = "2024"
license = "MIT"
[lints]
workspace = true
[dependencies]
libc = "0.2.172"

View File

@ -0,0 +1,113 @@
//! A shim for replacing some Crossterm methods with methods tied to a file descriptor.
use libc::termios as Termios;
use std::{os::fd::RawFd, io::IsTerminal};
/// The provided file descriptor was not a terminal.
#[derive(Debug)]
pub struct NotATerminal;
impl std::fmt::Display for NotATerminal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("The provided file descriptor is not a terminal")
}
}
impl std::error::Error for NotATerminal {}
/// A terminal controller.
pub struct TerminalIoctl {
fd: RawFd,
stored_termios: Option<Termios>,
}
type Result<T, E = std::io::Error> = std::result::Result<T, E>;
fn assert_io(result: i32) -> Result<i32> {
if result == -1 {
Err(std::io::Error::last_os_error())
} else {
Ok(result)
}
}
impl TerminalIoctl {
/// Construct a new controller for the given file descriptor.
///
/// # Errors
///
/// The method may return an error if the file descriptor is not a terminal.
pub fn new(fd: RawFd) -> Result<Self> {
// SAFETY: We do not invoke any function that closes the file descriptor, and
// the borrowed fd is dropped as the function returns.
let borrowed_fd = unsafe {
std::os::fd::BorrowedFd::borrow_raw(fd)
};
if !borrowed_fd.is_terminal() {
return Err(std::io::Error::other(NotATerminal));
}
Ok(Self {
fd,
stored_termios: None,
})
}
fn get_termios(&self) -> Result<Termios> {
let mut termios = unsafe { std::mem::zeroed() };
assert_io(unsafe { libc::tcgetattr(self.fd, &mut termios) })?;
Ok(termios)
}
/// Enable raw mode for the given terminal.
///
/// Replaces: [`crossterm::terminal::enable_raw_mode`].
///
/// # Errors
///
/// The method may return an error if
pub fn enable_raw_mode(&mut self) -> Result<()> {
if self.stored_termios.is_none() {
let mut termios = self.get_termios()?;
let original_mode_ios = termios;
unsafe { libc::cfmakeraw(&mut termios) };
assert_io(unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &termios) })?;
self.stored_termios = Some(original_mode_ios);
}
Ok(())
}
/// Disable raw mode for the given terminal.
///
/// Replaces: [`crossterm::terminal::disable_raw_mode`].
///
/// # Errors
///
/// The method may propagate errors encountered when interacting with the terminal.
pub fn disable_raw_mode(&mut self) -> Result<()> {
if let Some(termios) = self.stored_termios.take() {
assert_io(unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &termios) })?;
}
Ok(())
}
/// Return the size for the given terminal.
///
/// Replaces: [`crossterm::terminal::size`].
///
/// # Errors
///
/// The method may propagate errors encountered when interacting with the terminal.
pub fn size(&self) -> Result<(u16, u16)> {
let mut size = libc::winsize {
ws_row: 0,
ws_col: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
assert_io(unsafe { libc::ioctl(self.fd, libc::TIOCGWINSZ, &mut size) })?;
Ok((size.ws_col, size.ws_row))
}
}

View File

@ -1,42 +0,0 @@
# Build only pushed (merged) master or any pull request. This avoids the
# pull request to be build twice.
branches:
only:
- master
language: rust
rust:
- stable
- nightly
os:
- linux
- windows
- osx
git:
depth: 1
quiet: true
matrix:
allow_failures:
- rust: nightly
before_script:
- export PATH=$PATH:/home/travis/.cargo/bin
- rustup component add rustfmt
- rustup component add clippy
script:
- cargo fmt --version
- rustup --version
- rustc --version
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then cargo fmt --all -- --check; fi
- cargo clippy -- -D clippy::all
- cargo build
- cargo test --lib -- --nocapture --test-threads 1
- cargo test --lib --features serde -- --nocapture --test-threads 1
- cargo test --lib --features event-stream -- --nocapture --test-threads 1
- cargo test --all-features -- --nocapture --test-threads 1
- if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then cargo package; fi

View File

@ -1,744 +0,0 @@
# Version 0.27.1
## Added ⭐
- Add support for (de)serializing `Reset` `Color`
# Version 0.27
## Added ⭐
- Add `NO_COLOR` support (https://no-color.org/)
- Add option to force overwrite `NO_COLOR` (#802)
- Add support for scroll left/right events on windows and unix systems (#788).
- Add `window_size` function to fetch pixel width/height of screen for more sophisticated rendering in terminals.
- Add support for deserializing hex color strings to `Color` e.g #fffff.
## Changes
- Make the events module an optional feature `events` (to make crossterm more lightweight) (#776)
## Breaking ⚠️
- Set minimum rustc version to 1.58 (#798)
- Change all error types to `std::io::Result` (#765)
# Version 0.26.1
## Added ⭐
- Add synchronized output/update control (#756)
- Add kitty report alternate keys functionality (#754)
- Updates dev dependencies.
## Fixed 🐛
- Fix icorrect return in kitty keyboard enhancement check (#751)
- Fix panic when using `use-dev-tty` feature flag (#762)
# Version 0.26.0
## Added ⭐
- Add `SetCursorStyle` to set the cursor apearance and visibility. (#742)
- Add a function to check if kitty keyboard enhancement protocol is available. (#732)
- Add filedescriptors poll in order to move away from mio in the future (can be used via `use-dev-tty`). (#735)
## Fixed 🐛
- Improved F1-F4 handling for kitty keyboard protocol. (#736)
- Improved parsing of event types/modifiers with certain keys for kitty protocol. (#716)
## Breaking ⚠️
- Remove `SetCursorShape` in favour of `SetCursorStyle`. (#742)
- Make Windows resize event match `terminal::size` (#714)
- Rust 1.58 or later is now required.
- Add key release event for windows. (#745)
# Version 0.25.0
BREAKING: `Copy` trait is removed from `Event`, you can keep it by removing the "bracked-paste" feature flag. However this flag might be standardized in the future.
We removed the `Copy` from `Event` because the new `Paste` event, which contains a pasted string into the terminal, which is a non-copy string.
- Add ability to paste a string in into the terminal and fetch the pasted string via events (see `Event::Paste` and `EnableBracketedPaste `).
- Add support for functional key codes from kitty keyboard protocol. Try out by `PushKeyboardEnhancementFlags`. This protocol allows for:
- See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#modifiers
- Press, Repeat, Release event kinds.
- SUPER, HYPER, META modifiers.
- Media keycodes
- Right/left SHIFT, Control, Alt, Super, Hyper, Meta
- IsoLevel3Shift, IsoLevel5Shift
- Capslock, scroll lock, numlock
- Printscreen, pauze, menue, keyboard begin.
- Create `SetStyle` command to allow setting various styling in one command.
- Terminal Focus events (see `Event::FocusGained` and `Event::FocusLost`)
# Version 0.24.0
- Add DoubleUnderlined, Undercurled, Underdots the text, Underdotted, Underdashes, Underdashed attributes and allow coloring their foreground / background color.
- Fix windows unicode character parsing, this fixed various key combinations and support typing unicode characters.
- Consistency and better documentation on mouse cursor operations (BREAKING CHANGE).
- MoveTo, MoveToColumn, MoveToRow are 0-based. (left top most cell is 0,0). Moving like this is absolute
- MoveToNextLine, MoveToPreviousLine, MoveUp, MoveDown, MoveRight, MoveLeft are 1-based,. Moving like this is relative. Moving 1 left means moving 1 left. Moving 0 to the left is not possible, wikipedia states that most terminals will just default to 1.
- terminal::size returns error when previously it returned (0,0).
- Remove println from serialisation code.
- Fix mouse up for middle and right buttons.
- Fix escape codes on Git-Bash + Windows Terminal / Alacritty / WezTerm.
- Add support for cursor keys in application mode.
# Version 0.23.2
- Update signal-hook and mio to version 0.8.
# Version 0.23.1
- Fix control key parsing problem.
# Version 0.23
- Update dependencies.
- Add 0 check for all cursor functions to prevent undefined behaviour.
- Add CSIu key parsing for unix.
- Improve control character window key parsing supporting (e.g. CTRL [ and ])
- Update library to 2021 edition.
# Version 0.22.1
- Update yanked version crossterm-winapi and move to crossterm-winapi 0.9.0.
- Changed panic to error when calling disable-mouse capture without setting it first.
- Update bitflags dependency.
# Version 0.22
- Fix serde Color serialisation/deserialization inconsistency.
- Update crossterm-winapi 0.8.1 to fix panic for certain mouse events
# Version 0.21
- Expose `is_raw` function.
- Add 'purge' option on unix system, this clears the entire screen buffer.
- Improve serialisation for color enum values.
# Version 0.20
- Update from signal-hook with 'mio-feature flag' to signal-hook-mio 0.2.1.
- Manually implements Eq, PartialEq and Hash for KeyEvent improving equality checks and hash calculation.
- `crossterm::ErrorKind` to `io::Error`.
- Added Cursor Shape Support.
- Add support for function keys F13...F20.
- Support taking any Display in `SetTitle` command.
- Remove lazy_static dependency.
- Remove extra Clone bounds in the style module.
- Add `MoveToRow` command.
- Remove writer parameter from execute_winapi
# Version 0.19
- Use single thread for async event reader.
- Patch timeout handling for event polling this was not working correctly.
- Add unix support for more key combinations mainly complex ones with ALT/SHIFT/CTRL.
- Derive `PartialEq` and `Eq` for ContentStyle
- Fix windows resize event size, this used to be the buffer size but is screen size now.
- Change `Command::ansi_code` to `Command::write_ansi`, this way the ansi code will be written to given formatter.
# Version 0.18.2
- Fix panic when only setting bold and redirecting stdout.
- Use `tty_fd` for set/get terminal attributes
# Version 0.18.1
- Fix enabling ANSI support when stdout is redirected
- Update crossterm-winapi to 0.6.2
# Version 0.18.0
- Fix get position bug
- Fix windows 8 or lower write to user-given stdout instead of stdout.
- Make MoveCursor(Left/Right/Up/Dow) command with input 0 not move.
- Switch to futures-core to reduce dependencies.
- Command API restricts to only accept `std::io::Write`
- Make `supports_ansi` public
- Implement ALT + numbers windows systems.
# Version 0.17.7
- Fix cursor position retrieval bug linux.
# Version 0.17.6
- Add functionality to retrieve color based on passed ansi code.
- Switch from 'futures' to 'futures-util' crate to reduce dependency count
- Mio 0.7 update
- signal-hook update
- Make windows raw_mode act on CONIN$
- Added From<(u8, u8, u8)> Trait to Color::Rgb Enum
- Implement Color::try_from()
- Implement styler traits for `&'a str`
# Version 0.17.5
- Improved support of keymodifier for linux, arrow keys, function keys, home keys etc.
- Add `SetTitle` command to change the terminal title.
- Mio 0.7 update
# Version 0.17.4
- Add macros for `Colorize` and `Styler` impls, add an impl for `String`
- Add shift modifier to uppercase char events on unix
# Version 0.17.3
- Fix get terminal size mac os, this did not report the correct size.
# Version 0.17.2
- Windows unicode support
# Version 0.17.1
- Reverted bug in 0.17.0: "Make terminal size function fallback to `STDOUT_FILENO` if `/dev/tty` is missing.".
- Support for querying whether the current instance is a TTY.
# Version 0.17
- Impl Display for MoveToColumn, MoveToNextLine, MoveToPreviousLine
- Make unix event reader always use `/dev/tty`.
- Direct write command ansi_codes into formatter instead of double allocation.
- Add NONE flag to KeyModifiers
- Add support for converting chars to StylizedContent
- Make terminal size function fallback to `STDOUT_FILENO` if `/dev/tty` is missing.
# Version 0.16.0
- Change attribute vector in `ContentStyle` to bitmask.
- Add `SetAttributes` command.
- Add `Attributes` type, which is a bitfield of enabled attributes.
- Remove `exit()`, was useless.
# Version 0.15.0
- Fix CTRL + J key combination. This used to return an ENTER event.
- Add a generic implementation `Command` for `&T: Command`. This allows commands to be queued by reference, as well as by value.
- Remove unnecessary `Clone` trait bounds from `StyledContent`.
- Add `StyledContent::style_mut`.
- Handle error correctly for `execute!` and `queue!`.
- Fix minor syntax bug in `execute!` and `queue!`.
- Change `ContentStyle::apply` to take self by value instead of reference, to prevent an unnecessary extra clone.
- Added basic trait implementations (`Debug`, `Clone`, `Copy`, etc) to all of the command structs
- `ResetColor` uses `&'static str` instead of `String`
# Version 0.14.2
- Fix TIOCGWINSZ for FreeBSD
# Version 0.14.1
- Made windows cursor position relative to the window instead absolute to the screen buffer windows.
- Fix windows bug with `queue` macro were it consumed a type and required an type to be `Copy`.
# Version 0.14
- Replace the `input` module with brand new `event` module
- Terminal Resize Events
- Advanced modifier (SHIFT | ALT | CTRL) support for both mouse and key events and
- futures Stream (feature 'event-stream')
- Poll/read API
- It's **highly recommended** to read the
[Upgrade from 0.13 to 0.14](https://github.com/crossterm-rs/crossterm/wiki/Upgrade-from-0.13-to-0.14)
documentation
- Replace `docs/UPGRADE.md` with the [Upgrade Paths](https://github.com/crossterm-rs/crossterm/wiki#upgrade-paths)
documentation
- Add `MoveToColumn`, `MoveToPreviousLine`, `MoveToNextLine` commands
- Merge `screen` module into `terminal`
- Remove `screen::AlternateScreen`
- Remove `screen::Rawscreen`
* Move and rename `Rawscreen::into_raw_mode` and `Rawscreen::disable_raw_mode` to `terminal::enable_raw_mode` and `terminal::disable_raw_mode`
- Move `screen::EnterAlternateScreen` and `screen::LeaveAlternateScreen` to `terminal::EnterAlternateScreen` and `terminal::LeaveAlternateScreen`
- Replace `utils::Output` command with `style::Print` command
- Fix enable/disable mouse capture commands on Windows
- Allow trailing comma `queue!` & `execute!` macros
# Version 0.13.3
- Remove thread from AsyncReader on Windows.
- Improve HANDLE management windows.
# Version 0.13.2
- New `input::stop_reading_thread()` function
- Temporary workaround for the UNIX platform to stop the background
reading thread and close the file descriptor
- This function will be removed in the next version
# Version 0.13.1
- Async Reader fix, join background thread and avoid looping forever on windows.
# Version 0.13.0
**Major API-change, removed old-api**
- Remove `Crossterm` type
- Remove `TerminalCursor`, `TerminalColor`, `Terminal`
- Remove `cursor()`, `color()` , `terminal()`
- Remove re-exports at root, accessible via `module::types` (`cursor::MoveTo`)
- `input` module
- Derive 'Copy' for 'KeyEvent'
- Add the `EnableMouseCapture` and `EnableMouseCapture` commands
- `cursor` module
- Introduce static function `crossterm::cursor::position` in place of `TerminalCursor::pos`
- Rename `Goto` to `MoveTo`
- Rename `Up` to `MoveLeft`
- Rename `Right` to `MoveRight`
- Rename `Down` to `MoveDown`
- Rename `BlinkOn` to `EnableBlinking`
- Rename `BlinkOff` to `DisableBlinking`
- Rename `ResetPos` to `ResetPosition`
- Rename `SavePos` to `SavePosition`
- `terminal`
- Introduce static function `crossterm::terminal::size` in place of `Terminal::size`
- Introduce static function `crossterm::terminal::exit` in place of `Terminal::exit`
- `style module`
- Rename `ObjectStyle` to `ContentStyle`. Now full names are used for methods
- Rename `StyledObject` to `StyledContent` and made members private
- Rename `PrintStyledFont` to `PrintStyledContent`
- Rename `attr` method to `attribute`.
- Rename `Attribute::NoInverse` to `NoReverse`
- Update documentation
- Made `Colored` private, user should use commands instead
- Rename `SetFg` -> `SetForegroundColor`
- Rename `SetBg` -> `SetBackgroundColor`
- Rename `SetAttr` -> `SetAttribute`
- Rename `ContentStyle::fg_color` -> `ContentStyle::foreground_color`
- Rename `ContentStyle::bg_color` -> `ContentStyle::background_color`
- Rename `ContentStyle::attrs` -> `ContentStyle::attributes`
- Improve documentation
- Unix terminal size calculation with TPUT
# Version 0.12.1
- Move all the `crossterm_` crates code was moved to the `crossterm` crate
- `crossterm_cursor` is in the `cursor` module, etc.
- All these modules are public
- No public API breaking changes
# Version 0.12.0
- Following crates are deprecated and no longer maintained
- `crossterm_cursor`
- `crossterm_input`
- `crossterm_screen`
- `crossterm_style`
- `crossterm_terminal`
- `crossterm_utils`
## `crossterm_cursor` 0.4.0
- Fix examples link ([PR #6](https://github.com/crossterm-rs/crossterm-cursor/pull/6))
- Sync documentation style ([PR #7](https://github.com/crossterm-rs/crossterm-cursor/pull/7))
- Remove all references to the crossterm book ([PR #8](https://github.com/crossterm-rs/crossterm-cursor/pull/8))
- Replace `RAW_MODE_ENABLED` with `is_raw_mode_enabled` ([PR #9](https://github.com/crossterm-rs/crossterm-cursor/pull/9))
- Use `SyncReader` & `InputEvent::CursorPosition` for `pos_raw()` ([PR #10](https://github.com/crossterm-rs/crossterm-cursor/pull/10))
## `crossterm_input` 0.5.0
- Sync documentation style ([PR #4](https://github.com/crossterm-rs/crossterm-input/pull/4))
- Sync `SyncReader::next()` Windows and UNIX behavior ([PR #5](https://github.com/crossterm-rs/crossterm-input/pull/5))
- Remove all references to the crossterm book ([PR #6](https://github.com/crossterm-rs/crossterm-input/pull/6))
- Mouse coordinates synchronized with the cursor ([PR #7](https://github.com/crossterm-rs/crossterm-input/pull/7))
- Upper/left reported as `(0, 0)`
- Fix bug that read sync didn't block (Windows) ([PR #8](https://github.com/crossterm-rs/crossterm-input/pull/8))
- Refactor UNIX readers ([PR #9](https://github.com/crossterm-rs/crossterm-input/pull/9))
- AsyncReader produces mouse events
- One reading thread per application, not per `AsyncReader`
- Cursor position no longer consumed by another `AsyncReader`
- Implement sync reader for read_char (requires raw mode)
- Fix `SIGTTIN` when executed under the LLDB
- Add mio for reading from FD and more efficient polling (UNIX only)
- Sync UNIX and Windows vertical mouse position ([PR #11](https://github.com/crossterm-rs/crossterm-input/pull/11))
- Top is always reported as `0`
## `crossterm_screen` 0.3.2
- `to_alternate` switch back to main screen if it fails to switch into raw mode ([PR #4](https://github.com/crossterm-rs/crossterm-screen/pull/4))
- Improve the documentation ([PR #5](https://github.com/crossterm-rs/crossterm-screen/pull/5))
- Public API
- Include the book content in the documentation
- Remove all references to the crossterm book ([PR #6](https://github.com/crossterm-rs/crossterm-screen/pull/6))
- New commands introduced ([PR #7](https://github.com/crossterm-rs/crossterm-screen/pull/7))
- `EnterAlternateScreen`
- `LeaveAlternateScreen`
- Sync Windows and UNIX raw mode behavior ([PR #8](https://github.com/crossterm-rs/crossterm-screen/pull/8))
## `crossterm_style` 0.5.2
- Refactor ([PR #2](https://github.com/crossterm-rs/crossterm-style/pull/2))
- Added unit tests
- Improved documentation and added book page to `lib.rs`
- Fixed bug with `SetBg` command, WinApi logic
- Fixed bug with `StyledObject`, used stdout for resetting terminal color
- Introduced `ResetColor` command
- Sync documentation style ([PR #3](https://github.com/crossterm-rs/crossterm-style/pull/3))
- Remove all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-style/pull/4))
- Windows 7 grey/white foreground/intensity swapped ([PR #5](https://github.com/crossterm-rs/crossterm-style/pull/5))
## `crossterm_terminal` 0.3.2
- Removed `crossterm_cursor::sys` dependency ([PR #2](https://github.com/crossterm-rs/crossterm-terminal/pull/2))
- Internal refactoring & documentation ([PR #3](https://github.com/crossterm-rs/crossterm-terminal/pull/3))
- Removed all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-terminal/pull/4))
## `crossterm_utils` 0.4.0
- Add deprecation note ([PR #3](https://github.com/crossterm-rs/crossterm-utils/pull/3))
- Remove all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-utils/pull/4))
- Remove unsafe static mut ([PR #5](https://github.com/crossterm-rs/crossterm-utils/pull/5))
- `sys::unix::RAW_MODE_ENABLED` replaced with `sys::unix::is_raw_mode_enabled()` (breaking)
- New `lazy_static` dependency
## `crossterm_winapi` 0.3.0
- Make read sync block for windows systems ([PR #2](https://github.com/crossterm-rs/crossterm-winapi/pull/2))
# Version 0.11.1
- Maintenance release
- All sub-crates were moved to their own repositories in the `crossterm-rs` organization
# Version 0.11.0
As a preparation for crossterm 0.1.0 we have moved crossterm to an organisation called 'crossterm-rs'.
### Code Quality
- Code Cleanup: [warning-cleanup], [crossterm_style-cleanup], [crossterm_screen-cleanup], [crossterm_terminal-cleanup], [crossterm_utils-cleanup], [2018-cleanup], [api-cleanup-1], [api-cleanup-2], [api-cleanup-3]
- Examples: [example-cleanup_1], [example-cleanup_2], [example-fix], [commandbar-fix], [snake-game-improved]
- Fixed all broken tests and added tests
### Important Changes
- Return written bytes: [return-written-bytes]
- Added derives: `Debug` for `ObjectStyle` [debug-derive], Serialize/Deserialize for key events [serde]
- Improved error handling:
- Return `crossterm::Result` from all api's: [return_crossterm_result]
* `TerminalCursor::pos()` returns `Result<(u16, u16)>`
* `Terminal::size()` returns `Result<(u16, u16)>`
* `TerminalCursor::move_*` returns `crossterm::Result`
* `ExecutableCommand::queue` returns `crossterm::Result`
* `QueueableCommand::queue` returns `crossterm::Result`
* `get_available_color_count` returns no result
* `RawScreen::into_raw_mode` returns `crossterm::Result` instead of `io::Result`
* `RawScreen::disable_raw_mode` returns `crossterm::Result` instead of `io::Result`
* `AlternateScreen::to_alternate` returns `crossterm::Result` instead of `io::Result`
* `TerminalInput::read_line` returns `crossterm::Result` instead of `io::Result`
* `TerminalInput::read_char` returns `crossterm::Result` instead of `io::Result`
* Maybe I forgot something, a lot of functions have changed
- Removed all unwraps/expects from library
- Add KeyEvent::Enter and KeyEvent::Tab: [added-key-event-enter], [added-key-event-tab]
- Sync set/get terminal size behaviour: [fixed-get-set-terminal-size]
- Method renames:
* `AsyncReader::stop_reading()` to `stop()`
* `RawScreen::disable_raw_mode_on_drop` to `keep_raw_mode_on_drop`
* `TerminalCursor::reset_position()` to `restore_position()`
* `Command::get_anis_code()` to `ansi_code()`
* `available_color_count` to `available_color_count()`
* `Terminal::terminal_size` to `Terminal::size`
* `Console::get_handle` to `Console::handle`
- All `i16` values for indexing: set size, set cursor pos, scrolling synced to `u16` values
- Command API takes mutable self instead of self
[serde]: https://github.com/crossterm-rs/crossterm/pull/190
[debug-derive]: https://github.com/crossterm-rs/crossterm/pull/192
[example-fix]: https://github.com/crossterm-rs/crossterm/pull/193
[commandbar-fix]: https://github.com/crossterm-rs/crossterm/pull/204
[warning-cleanup]: https://github.com/crossterm-rs/crossterm/pull/198
[example-cleanup_1]: https://github.com/crossterm-rs/crossterm/pull/196
[example-cleanup_2]: https://github.com/crossterm-rs/crossterm/pull/225
[snake-game-improved]: https://github.com/crossterm-rs/crossterm/pull/231
[crossterm_style-cleanup]: https://github.com/crossterm-rs/crossterm/pull/208
[crossterm_screen-cleanup]: https://github.com/crossterm-rs/crossterm/pull/209
[crossterm_terminal-cleanup]: https://github.com/crossterm-rs/crossterm/pull/210
[crossterm_utils-cleanup]: https://github.com/crossterm-rs/crossterm/pull/211
[2018-cleanup]: https://github.com/crossterm-rs/crossterm/pull/222
[wild-card-cleanup]: https://github.com/crossterm-rs/crossterm/pull/224
[api-cleanup-1]: https://github.com/crossterm-rs/crossterm/pull/235
[api-cleanup-2]: https://github.com/crossterm-rs/crossterm/pull/238
[api-cleanup-3]: https://github.com/crossterm-rs/crossterm/pull/240
[return-written-bytes]: https://github.com/crossterm-rs/crossterm/pull/212
[return_crossterm_result]: https://github.com/crossterm-rs/crossterm/pull/232
[added-key-event-tab]: https://github.com/crossterm-rs/crossterm/pull/239
[added-key-event-enter]: https://github.com/crossterm-rs/crossterm/pull/236
[fixed-get-set-terminal-size]: https://github.com/crossterm-rs/crossterm/pull/242
# Version 0.10.1
# Version 0.10.0 ~ yanked
- Implement command API, to have better performance and more control over how and when commands are executed. [PR](https://github.com/crossterm-rs/crossterm/commit/1a60924abd462ab169b6706aab68f4cca31d7bc2), [issue](https://github.com/crossterm-rs/crossterm/issues/171)
- Fix showing, hiding cursor windows implementation
- Remove some of the parsing logic from windows keys to ansi codes to key events [PR](https://github.com/crossterm-rs/crossterm/commit/762c3a9b8e3d1fba87acde237f8ed09e74cd9ecd)
- Made terminal size 1-based [PR](https://github.com/crossterm-rs/crossterm/commit/d689d7e8ed46a335474b8262bd76f21feaaf0c50)
- Add some derives
# Version 0.9.6
- Copy for KeyEvent
- CTRL + Left, Down, Up, Right key support
- SHIFT + Left, Down, Up, Right key support
- Fixed UNIX cursor position bug [issue](https://github.com/crossterm-rs/crossterm/issues/140), [PR](https://github.com/crossterm-rs/crossterm/pull/152)
# Version 0.9.5
- Prefetch buffer size for more efficient windows input reads. [PR](https://github.com/crossterm-rs/crossterm/pull/144)
# Version 0.9.4
- Reset foreground and background color individually. [PR](https://github.com/crossterm-rs/crossterm/pull/138)
- Backtap input support. [PR](https://github.com/crossterm-rs/crossterm/pull/129)
- Corrected white/grey and added dark grey.
- Fixed getting cursor position with raw screen enabled. [PR](https://github.com/crossterm-rs/crossterm/pull/134)
- Removed one redundant stdout lock
# Version 0.9.3
- Removed println from `SyncReader`
## Version 0.9.2
- Terminal size linux was not 0-based
- Windows mouse input event position was 0-based and should be 1-based
- Result, ErrorKind are made re-exported
- Fixed some special key combination detections for UNIX systems
- Made FreeBSD compile
## Version 0.9.1
- Fixed libc compile error
## Version 0.9.0 (yanked)
This release is all about moving to a stabilized API for 1.0.
- Major refactor and cleanup.
- Improved performance;
- No locking when writing to stdout.
- UNIX doesn't have any dynamic dispatch anymore.
- Windows has improved the way to check if ANSI modes are enabled.
- Removed lot's of complex API calls: `from_screen`, `from_output`
- Removed `Arc<TerminalOutput>` from all internal Api's.
- Removed termios dependency for UNIX systems.
- Upgraded deps.
- Removed about 1000 lines of code
- `TerminalOutput`
- `Screen`
- unsafe code
- Some duplicated code introduced by a previous refactor.
- Raw modes UNIX systems improved
- Added `NoItalic` attribute
## Version 0.8.2
- Bug fix for sync reader UNIX.
## Version 0.8.1
- Added public re-exports for input.
# Version 0.8.0
- Introduced KeyEvents
- Introduced MouseEvents
- Upgraded crossterm_winapi 0.2
# Version 0.7.0
- Introduced more `Attributes`
- Introduced easier ways to style text [issue 87](https://github.com/crossterm-rs/crossterm/issues/87).
- Removed `ColorType` since it was unnecessary.
# Version 0.6.0
- Introduced feature flags; input, cursor, style, terminal, screen.
- All modules are moved to their own crate.
- Introduced crossterm workspace
- Less dependencies.
- Improved namespaces.
[PR 84](https://github.com/crossterm-rs/crossterm/pull/84)
# Version 0.5.5
- Error module is made public [PR 78](https://github.com/crossterm-rs/crossterm/pull/78).
# Version 0.5.4
- WinApi rewrite and correctly error handled [PR 67](https://github.com/crossterm-rs/crossterm/pull/67)
- Windows attribute support [PR 62](https://github.com/crossterm-rs/crossterm/pull/62)
- Readline bug fix windows systems [PR 62](https://github.com/crossterm-rs/crossterm/pull/62)
- Error handling improvement.
- General refactoring, all warnings removed.
- Documentation improvement.
# Version 0.5.1
- Documentation refactor.
- Fixed broken API documentation [PR 53](https://github.com/crossterm-rs/crossterm/pull/53).
# Version 0.5.0
- Added ability to pause the terminal [issue](https://github.com/crossterm-rs/crossterm/issues/39)
- RGB support for Windows 10 systems
- ANSI color value (255) color support
- More convenient API, no need to care about `Screen` unless working with when working with alternate or raw screen [PR](https://github.com/crossterm-rs/crossterm/pull/44)
- Implemented Display for styled object
# Version 0.4.3
- Fixed bug [issue 41](https://github.com/crossterm-rs/crossterm/issues/41)
# Version 0.4.2
- Added functionality to make a styled object writable to screen [issue 33](https://github.com/crossterm-rs/crossterm/issues/33)
- Added unit tests.
- Bugfix with getting terminal size unix.
- Bugfix with returning written bytes [pull request 31](https://github.com/crossterm-rs/crossterm/pull/31)
- removed methods calls: `as_any()` and `as_any_mut()` from `TerminalOutput`
# Version 0.4.1
- Fixed resizing of ansi terminal with and height where in the wrong order.
# Version 0.4.0
- Input support (read_line, read_char, read_async, read_until_async)
- Styling module improved
- Everything is multithreaded (`Send`, `Sync`)
- Performance enhancements: removed mutexes, removed state manager, removed context type removed unnecessarily RC types.
- Bug fix resetting console color.
- Bug fix whit undoing raw modes.
- More correct error handling.
- Overall command improvement.
- Overall refactor of code.
# Version 0.3.0
This version has some braking changes check [upgrade manual](UPGRADE%20Manual.md) for more information about what is changed.
I think you should not switch to version `0.3.0` if you aren't going to use the AlternateScreen feature.
Because you will have some work to get to the new version of crossterm depending on your situation.
Some Features crossterm 0.3.0
- Alternate Screen for windows and unix systems.
- Raw screen for unix and windows systems [Issue 5](https://github.com/crossterm-rs/crossterm/issues/5)..
- Hiding an showing the cursor.
- Control over blinking of the terminal cursor (only some terminals are supporting this).
- The terminal state will be set to its original state when process ends [issue7](https://github.com/crossterm-rs/crossterm/issues/7).
- exit the current process.
## Alternate screen
This create supports alternate screen for both windows and unix systems. You can use
*Nix style applications often utilize an alternate screen buffer, so that they can modify the entire contents of the buffer, without affecting the application that started them.
The alternate buffer is exactly the dimensions of the window, without any scrollback region.
For an example of this behavior, consider when vim is launched from bash.
Vim uses the entirety of the screen to edit the file, then returning to bash leaves the original buffer unchanged.
I Highly recommend you to check the `examples/program_examples/first_depth_search` for seeing this in action.
## Raw screen
This crate now supports raw screen for both windows and unix systems.
What exactly is raw state:
- No line buffering.
Normally the terminals uses line buffering. This means that the input will be send to the terminal line by line.
With raw mode the input will be send one byte at a time.
- Input
All input has to be written manually by the programmer.
- Characters
The characters are not processed by the terminal driver, but are sent straight through.
Special character have no meaning, like backspace will not be interpret as backspace but instead will be directly send to the terminal.
With these modes you can easier design the terminal screen.
## Some functionalities added
- Hiding and showing terminal cursor
- Enable or disabling blinking of the cursor for unix systems (this is not widely supported)
- Restoring the terminal to original modes.
- Added a [wrapper](https://github.com/crossterm-rs/crossterm/blob/master/src/shared/crossterm.rs) for managing all the functionalities of crossterm `Crossterm`.
- Exit the current running process
## Examples
Added [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) for each version of the crossterm version.
Also added a folder with some [real life examples](https://github.com/crossterm-rs/crossterm/tree/master/examples/program_examples).
## Context
What is the `Context` all about? This `Context` has several reasons why it is introduced into `crossterm version 0.3.0`.
These points are related to the features like `Alternatescreen` and managing the terminal state.
- At first `Terminal state`:
Because this is a terminal manipulating library there will be made changes to terminal when running an process.
If you stop the process you want the terminal back in its original state.
Therefore, I need to track the changes made to the terminal.
- At second `Handle to the console`
In Rust we can use `stdout()` to get an handle to the current default console handle.
For example when in unix systems you want to print something to the main screen you can use the following code:
write!(std::io::stdout(), "{}", "some text").
But things change when we are in alternate screen modes.
We can not simply use `stdout()` to get a handle to the alternate screen, since this call returns the current default console handle (handle to mainscreen).
Because of that we need to store an handle to the current screen.
This handle could be used to put into alternate screen modes and back into main screen modes.
Through this stored handle Crossterm can execute its command and write on and to the current screen whether it be alternate screen or main screen.
For unix systems we store the handle gotten from `stdout()` for windows systems that are not supporting ANSI escape codes we store WinApi `HANDLE` struct witch will provide access to the current screen.
So to recap this `Context` struct is a wrapper for a type that manges terminal state changes.
When this `Context` goes out of scope all changes made will be undone.
Also is this `Context` is a wrapper for access to the current console screen.
Because Crossterm needs access to the above to types quite often I have chosen to add those two in one struct called `Context` so that this type could be shared throughout library.
Check this link for more info: [cleanup of rust code](https://stackoverflow.com/questions/48732387/how-can-i-run-clean-up-code-in-a-rust-library).
More info over writing to alternate screen buffer on windows and unix see this [link](https://github.com/crossterm-rs/crossterm/issues/17)
__Now the user has to pass an context type to the modules of Crossterm like this:__
let context = Context::new();
let cursor = cursor(&context);
let terminal = terminal(&context);
let color = color(&context);
Because this looks a little odd I will provide a type widths will manage the `Context` for you. You can call the different modules like the following:
let crossterm = Crossterm::new();
let color = crossterm.color();
let cursor = crossterm.cursor();
let terminal = crossterm.terminal();
### Alternate screen
When you want to switch to alternate screen there are a couple of things to keep in mind for it to work correctly.
First off some code of how to switch to Alternate screen, for more info check the [alternate screen example](https://github.com/crossterm-rs/crossterm/blob/master/examples/alternate_screen.rs).
_Create alternate screen from `Context`_
// create context.
let context = crossterm::Context::new();
// create instance of Alternatescreen by the given context, this will also switch to it.
let mut screen = crossterm::AlternateScreen::from(context.clone());
// write to the alternate screen.
write!(screen, "test");
_Create alternate screen from `Crossterm`:_
// create context.
let crossterm = ::crossterm::Crossterm::new();
// create instance of Alternatescreen by the given reference to crossterm, this will also switch to it.
let mut screen = crossterm::AlternateScreen::from(&crossterm);
// write to the alternate screen.
write!(screen, "test");
like demonstrated above, to get the functionalities of `cursor(), color(), terminal()` also working on alternate screen.
You need to pass it the same `Context` as you have passed to the previous three called functions,
If you don't use the same `Context` in `cursor(), color(), terminal()` than these modules will be using the main screen and you will not see anything at the alternate screen. If you use the [Crossterm](https://github.com/crossterm-rs/crossterm/blob/master/src/shared/crossterm.rs) type you can get the `Context` from it by calling the crossterm.get_context() whereafter you can create the AlternateScreen from it.
# Version 0.2.2
- Bug see [issue 15](https://github.com/crossterm-rs/crossterm/issues/15)
# Version 0.2.1
- Default ANSI escape codes for windows machines, if windows does not support ANSI switch back to WinApi.
- method grammar mistake fixed [Issue 3](https://github.com/crossterm-rs/crossterm/issues/3)
- Some Refactorings in method names see [issue 4](https://github.com/crossterm-rs/crossterm/issues/4)
- Removed bin reference from crate [Issue 6](https://github.com/crossterm-rs/crossterm/issues/6)
- Get position unix fixed [issue 8](https://github.com/crossterm-rs/crossterm/issues/8)
# Version 0.2
- 256 color support.
- Text Attributes like: bold, italic, underscore and crossed word etc.
- Custom ANSI color code input to set fore- and background color for unix.
- Storing the current cursor position and resetting to that stored cursor position later.
- Resizing the terminal.

View File

@ -1,97 +0,0 @@
[package]
name = "keyfork-crossterm"
version = "0.27.2"
# authors = ["T. Post"]
authors = ["Ryan Heywood <ryan@distrust.co>"]
description = "A crossplatform terminal library for manipulating terminals."
repository = "https://git.distrust.co/public/keyfork"
# documentation = "https://docs.rs/crossterm/"
license = "MIT"
# keywords = ["event", "color", "cli", "input", "terminal"]
# exclude = ["target", "Cargo.lock"]
readme = "README.md"
edition = "2021"
rust-version = "1.58.0"
# categories = ["command-line-interface", "command-line-utilities"]
# [lib]
# name = "crossterm"
# path = "src/lib.rs"
# Build documentation with all features -> EventStream is available
[package.metadata.docs.rs]
all-features = true
# Features
[features]
default = ["bracketed-paste", "windows", "events"]
windows = ["dep:winapi", "dep:crossterm_winapi"] # Disables winapi dependencies from being included into the binary (SHOULD NOT be disabled on windows).
bracketed-paste = [] # Enables triggering a `Event::Paste` when pasting text into the terminal.
event-stream = ["dep:futures-core", "events"] # Enables async events
use-dev-tty = ["filedescriptor"] # Enables raw file descriptor polling / selecting instead of mio.
events = ["dep:mio", "dep:signal-hook", "dep:signal-hook-mio"] # Enables reading input/events from the system.
serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types.
# Shared dependencies
[dependencies]
bitflags = {version = "2.3" }
parking_lot = "0.12"
# optional deps only added when requested
futures-core = { version = "0.3", optional = true, default-features = false }
serde = { version = "1.0", features = ["derive"], optional = true }
# Windows dependencies
[target.'cfg(windows)'.dependencies.winapi]
version = "0.3.9"
features = ["winuser", "winerror"]
optional = true
[target.'cfg(windows)'.dependencies]
crossterm_winapi = { version = "0.9.1", optional = true }
# UNIX dependencies
[target.'cfg(unix)'.dependencies]
libc = "0.2"
signal-hook = { version = "0.3.17", optional = true }
filedescriptor = { version = "0.8", optional = true }
mio = { version = "1.0", features = ["os-poll"], optional = true }
signal-hook-mio = { version = "0.2.3", features = ["support-v1_0"], optional = true }
# Dev dependencies (examples, ...)
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
futures = "0.3"
futures-timer = "3.0"
async-std = "1.12"
serde_json = { workspace = true }
serial_test = "2.0.0"
# Examples
[[example]]
name = "event-read"
required-features = ["bracketed-paste", "events"]
[[example]]
name = "event-match-modifiers"
required-features = ["bracketed-paste", "events"]
[[example]]
name = "event-poll-read"
required-features = ["bracketed-paste", "events"]
[[example]]
name = "event-stream-async-std"
required-features = ["event-stream", "events"]
[[example]]
name = "event-stream-tokio"
required-features = ["event-stream", "events"]
[[example]]
name = "event-read-char-line"
required-features = ["events"]
[[example]]
name = "stderr"
required-features = ["events"]

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 Timon
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1 +0,0 @@
Forked from https://github.com/crossterm-rs/crossterm

View File

@ -1,213 +0,0 @@
<h1 align="center"><img width="440" src="docs/crossterm_full.png" /></h1>
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2) ![Travis][s7] [![Latest Version][s1]][l1] [![MIT][s2]][l2] [![docs][s3]][l3] ![Lines of Code][s6] [![Join us on Discord][s5]][l5]
# Cross-platform Terminal Manipulation Library
Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces (see [features](#features)). It supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested,
see [Tested Terminals](#tested-terminals) for more info).
## Table of Contents
- [Cross-platform Terminal Manipulation Library](#cross-platform-terminal-manipulation-library)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Tested Terminals](#tested-terminals)
- [Getting Started](#getting-started)
- [Feature Flags](#feature-flags)
- [Dependency Justification](#dependency-justification)
- [Other Resources](#other-resources)
- [Used By](#used-by)
- [Contributing](#contributing)
- [Authors](#authors)
- [License](#license)
## Features
- Cross-platform
- Multi-threaded (send, sync)
- Detailed documentation
- Few dependencies
- Full control over writing and flushing output buffer
- Is tty
- Cursor
- Move the cursor N times (up, down, left, right)
- Move to previous / next line
- Move to column
- Set/get the cursor position
- Store the cursor position and restore to it later
- Hide/show the cursor
- Enable/disable cursor blinking (not all terminals do support this feature)
- Styled output
- Foreground color (16 base colors)
- Background color (16 base colors)
- 256 (ANSI) color support (Windows 10 and UNIX only)
- RGB color support (Windows 10 and UNIX only)
- Text attributes like bold, italic, underscore, crossed, etc
- Terminal
- Clear (all lines, current line, from cursor down and up, until new line)
- Scroll up, down
- Set/get the terminal size
- Exit current process
- Alternate screen
- Raw screen
- Set terminal title
- Enable/disable line wrapping
- Event
- Input Events
- Mouse Events (press, release, position, button, drag)
- Terminal Resize Events
- Advanced modifier (SHIFT | ALT | CTRL) support for both mouse and key events and
- futures Stream (feature 'event-stream')
- Poll/read API
<!--
WARNING: Do not change following heading title as it's used in the URL by other crates!
-->
### Tested Terminals
- Console Host
- Windows 10 (Pro)
- Windows 8.1 (N)
- Ubuntu Desktop Terminal
- Ubuntu 17.10
- Pop!_OS ( Ubuntu ) 20.04
- (Arch, Manjaro) KDE Konsole
- (Arch, NixOS) Kitty
- Linux Mint
- (OpenSuse) Alacritty
- (Chrome OS) Crostini
- Apple
- macOS Monterey 12.7.1 (Intel-Chip)
This crate supports all UNIX terminals and Windows terminals down to Windows 7; however, not all of the
terminals have been tested. If you have used this library for a terminal other than the above list without
issues, then feel free to add it to the above list - I really would appreciate it!
## Getting Started
_see the [examples directory](examples/) and [documentation](https://docs.rs/crossterm/) for more advanced examples._
<details>
<summary>
Click to show Cargo.toml.
</summary>
```toml
[dependencies]
crossterm = "0.27"
```
</details>
<p></p>
```rust
use std::io::{stdout, Write};
use crossterm::{
execute,
style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor},
ExecutableCommand, Result,
event,
};
fn main() -> std::io::Result<()> {
// using the macro
execute!(
stdout(),
SetForegroundColor(Color::Blue),
SetBackgroundColor(Color::Red),
Print("Styled text here."),
ResetColor
)?;
// or using functions
stdout()
.execute(SetForegroundColor(Color::Blue))?
.execute(SetBackgroundColor(Color::Red))?
.execute(Print("Styled text here."))?
.execute(ResetColor)?;
Ok(())
}
```
Checkout this [list](https://docs.rs/crossterm/latest/crossterm/index.html#supported-commands) with all possible commands.
### Feature Flags
```toml
[dependencies.crossterm]
version = "0.27"
features = ["event-stream"]
```
| Feature | Description |
|:---------------|:---------------------------------------------|
| `event-stream` | `futures::Stream` producing `Result<Event>`. |
| `serde` | (De)serializing of events. |
| `events` | Reading input/system events (enabled by default) |
| `filedescriptor` | Use raw filedescriptor for all events rather then mio dependency |
To use crossterm as a very thin layer you can disable the `events` feature or use `filedescriptor` feature.
This can disable `mio` / `signal-hook` / `signal-hook-mio` dependencies.
### Dependency Justification
| Dependency | Used for | Included |
|:---------------|:---------------------------------------------------------------------------------|:--------------------------------------|
| `bitflags` | `KeyModifiers`, those are differ based on input. | always |
| `parking_lot` | locking `RwLock`s with a timeout, const mutexes. | always |
| `libc` | UNIX terminal_size/raw modes/set_title and several other low level functionality. | optional (`events` feature), UNIX only |
| `Mio` | event readiness polling, waking up poller | optional (`events` feature), UNIX only |
| `signal-hook` | signal-hook is used to handle terminal resize SIGNAL with Mio. | optional (`events` feature),UNIX only |
| `winapi` | Used for low-level windows system calls which ANSI codes can't replace | windows only |
| `futures-core` | For async stream of events | only with `event-stream` feature flag |
| `serde` | ***ser***ializing and ***de***serializing of events | only with `serde` feature flag |
### Other Resources
- [API documentation](https://docs.rs/crossterm/)
- [Deprecated examples repository](https://github.com/crossterm-rs/examples)
## Used By
- [Broot](https://dystroy.org/broot/)
- [Cursive](https://github.com/gyscos/Cursive)
- [TUI](https://github.com/fdehau/tui-rs)
- [Rust-sloth](https://github.com/ecumene/rust-sloth)
- [Rusty-rain](https://github.com/cowboy8625/rusty-rain)
## Contributing
We highly appreciate when anyone contributes to this crate. Before you do, please,
read the [Contributing](docs/CONTRIBUTING.md) guidelines.
## Authors
* **Timon Post** - *Project Owner & creator*
## License
This project, `crossterm` and all its sub-crates: `crossterm_screen`, `crossterm_cursor`, `crossterm_style`,
`crossterm_input`, `crossterm_terminal`, `crossterm_winapi`, `crossterm_utils` are licensed under the MIT
License - see the [LICENSE](https://github.com/crossterm-rs/crossterm/blob/master/LICENSE) file for details.
[s1]: https://img.shields.io/crates/v/crossterm.svg
[l1]: https://crates.io/crates/crossterm
[s2]: https://img.shields.io/badge/license-MIT-blue.svg
[l2]: ./LICENSE
[s3]: https://docs.rs/crossterm/badge.svg
[l3]: https://docs.rs/crossterm/
[s3]: https://docs.rs/crossterm/badge.svg
[l3]: https://docs.rs/crossterm/
[s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord
[l5]: https://discord.gg/K4nyTDB
[s6]: https://tokei.rs/b1/github/crossterm-rs/crossterm?category=code
[s7]: https://travis-ci.org/crossterm-rs/crossterm.svg?branch=master

View File

@ -1 +0,0 @@
book

View File

@ -1,65 +0,0 @@
# Contributing
I would appreciate any contributions to this crate. However, some things are handy to know.
## Code Style
### Import Order
All imports are semantically grouped and ordered. The order is:
- standard library (`use std::...`)
- external crates (`use rand::...`)
- current crate (`use crate::...`)
- parent module (`use super::..`)
- current module (`use self::...`)
- module declaration (`mod ...`)
There must be an empty line between groups. An example:
```rust
use crossterm_utils::{csi, write_cout, Result};
use crate::sys::{get_cursor_position, show_cursor};
use super::Cursor;
```
#### CLion Tips
The CLion IDE does this for you (_Menu_ -> _Code_ -> _Optimize Imports_). Be aware that the CLion sorts
imports in a group in a different way when compared to the `rustfmt`. It's effectively two steps operation
to get proper grouping & sorting:
* _Menu_ -> _Code_ -> _Optimize Imports_ - group & semantically order imports
* `cargo fmt` - fix ordering within the group
Second step can be automated via _CLion_ -> _Preferences_ ->
_Languages & Frameworks_ -> _Rust_ -> _Rustfmt_ -> _Run rustfmt on save_.
### Max Line Length
| Type | Max line length |
|:---------------------|----------------:|
| Code | 100 |
| Comments in the code | 120 |
| Documentation | 120 |
100 is the [`max_width`](https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#max_width)
default value.
120 is because of the GitHub. The editor & viewer width there is +- 123 characters.
### Warnings
The code must be warning free. It's quite hard to find an error if the build logs are polluted with warnings.
If you decide to silent a warning with (`#[allow(...)]`), please add a comment why it's required.
Always consult the [Travis CI](https://travis-ci.org/crossterm-rs/crossterm/pull_requests) build logs.
### Forbidden Warnings
Search for `#![deny(...)]` in the code:
* `unused_must_use`
* `unused_imports`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,103 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="433.000000pt" viewBox="0 0 700.000000 433.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.15, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,433.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M987 3178 c-41 -74 -41 -74 -90 -34 -63 52 -83 48 -112 -19 -14 -30
-27 -55 -30 -55 -3 0 -24 14 -47 30 -43 32 -79 38 -95 18 -6 -7 -13 -35 -17
-62 -3 -27 -8 -51 -11 -54 -2 -2 -22 3 -45 12 -89 35 -118 16 -106 -69 4 -24
3 -46 -2 -49 -6 -3 -35 -1 -67 6 -48 9 -59 9 -72 -5 -13 -13 -14 -22 -4 -61
19 -75 19 -75 -57 -78 -61 -3 -67 -5 -70 -26 -2 -13 4 -41 12 -63 25 -61 22
-66 -36 -74 -29 -4 -59 -12 -65 -17 -22 -18 -14 -53 22 -95 19 -23 35 -43 35
-46 0 -2 -12 -7 -27 -10 -47 -10 -93 -44 -93 -69 0 -13 18 -40 41 -64 l41 -42
-46 -27 c-42 -24 -46 -30 -46 -64 0 -33 5 -40 40 -60 22 -12 40 -25 40 -28 0
-2 -16 -23 -35 -46 -53 -61 -46 -86 28 -112 31 -11 57 -24 57 -30 0 -5 -14
-23 -31 -39 -60 -60 -45 -109 37 -119 25 -2 48 -7 51 -11 3 -3 -2 -25 -11 -49
-32 -86 -16 -113 63 -102 31 4 51 3 56 -4 3 -6 1 -37 -5 -67 -16 -78 -4 -90
74 -74 30 6 60 9 65 5 6 -3 7 -27 4 -55 -4 -33 -2 -55 7 -65 16 -19 57 -19 90
0 46 26 59 20 66 -36 12 -86 45 -100 108 -44 22 19 45 35 51 35 6 0 19 -26 30
-58 26 -75 52 -80 112 -22 l43 40 26 -42 c31 -50 41 -58 71 -58 16 0 31 15 54
53 l33 52 39 -42 c50 -54 77 -56 106 -8 11 20 24 47 27 60 8 31 14 31 48 0 71
-66 107 -57 121 32 4 26 9 51 11 55 3 4 23 -2 47 -12 52 -24 93 -25 101 -4 3
9 6 40 6 70 0 61 1 61 76 43 39 -10 48 -9 61 4 14 13 14 24 5 72 -7 32 -9 62
-6 67 4 6 25 7 50 3 63 -9 84 0 84 36 0 16 -7 46 -15 66 -8 19 -15 39 -15 44
0 5 23 11 51 15 85 11 99 43 49 112 -16 23 -30 45 -30 49 0 5 24 17 53 28 68
27 73 53 22 112 -19 23 -35 44 -35 47 0 2 22 17 50 33 41 23 50 33 50 55 0 22
-9 32 -50 54 -27 15 -50 32 -50 39 0 6 16 26 35 45 19 19 35 44 35 56 0 25
-40 59 -83 69 -15 4 -27 10 -27 14 0 3 14 24 30 45 32 42 38 77 18 94 -7 5
-37 13 -66 17 -43 6 -53 10 -49 24 40 138 39 141 -48 141 -36 0 -65 4 -65 9 0
5 5 32 12 61 10 44 9 53 -5 67 -14 14 -23 15 -67 5 -29 -7 -55 -12 -60 -12 -5
0 -10 30 -12 68 -3 65 -4 67 -31 70 -16 2 -46 -4 -68 -12 -22 -9 -43 -16 -48
-16 -5 0 -12 24 -16 53 -4 28 -12 58 -17 65 -17 20 -53 14 -97 -19 -24 -17
-44 -29 -44 -27 -59 121 -75 130 -137 74 -22 -20 -44 -36 -48 -36 -4 0 -21 23
-37 50 -37 62 -70 69 -98 18z m41 -398 c17 0 44 18 83 55 32 30 66 55 76 55
32 0 125 -31 186 -61 l57 -29 0 -88 c0 -62 -4 -94 -15 -109 l-15 -22 -44 19
c-90 40 -166 54 -301 54 -105 0 -145 -4 -210 -23 -197 -55 -335 -178 -386
-341 -29 -92 -28 -227 1 -315 12 -37 20 -68 19 -69 -10 -9 -107 -66 -112 -66
-6 0 -33 66 -53 130 -8 25 -17 95 -21 156 l-6 110 32 13 c17 8 31 17 31 21 0
4 12 11 28 15 52 13 72 33 72 73 0 20 -7 50 -15 65 -22 44 -29 117 -13 154 31
74 177 207 285 262 23 11 72 29 110 40 l68 19 59 -59 c41 -41 66 -59 84 -59z
m206 -307 c68 -22 133 -83 168 -160 l28 -63 85 0 85 0 0 40 c0 39 0 40 19 21
10 -10 33 -22 50 -26 17 -4 31 -10 31 -15 0 -5 15 -14 34 -21 33 -12 35 -14
38 -73 10 -156 -34 -309 -117 -411 -19 -23 -35 -45 -35 -48 0 -3 -12 -18 -27
-33 -26 -26 -28 -27 -88 -15 -120 22 -129 17 -151 -81 -8 -35 -16 -73 -19 -85
-11 -47 -251 -92 -387 -73 -40 6 -80 12 -88 15 -131 36 -129 35 -139 68 -6 18
-15 56 -21 86 -5 30 -15 60 -21 68 -14 16 -75 17 -135 2 -39 -10 -48 -9 -62 5
-19 19 -23 15 63 65 l49 29 31 -23 c46 -36 143 -81 215 -101 90 -25 328 -26
420 -1 204 55 336 187 348 350 l4 57 -85 0 -86 0 -7 -37 c-23 -130 -113 -200
-274 -217 -182 -18 -314 57 -356 201 -24 82 -16 259 15 328 66 148 229 206
415 148z"/>
<path d="M4557 2523 c-4 -3 -7 -17 -7 -30 0 -59 -47 -109 -116 -123 -53 -11
-54 -12 -54 -46 l0 -34 50 0 50 0 0 -135 c0 -75 5 -145 11 -158 16 -36 64 -59
133 -64 115 -10 158 27 172 145 l7 62 -46 0 -46 0 -3 -47 c-3 -41 -6 -48 -25
-51 -23 -3 -23 -2 -23 122 l0 126 60 0 60 0 0 40 0 40 -60 0 -60 0 0 80 0 80
-48 0 c-27 0 -52 -3 -55 -7z"/>
<path d="M2258 2373 l-118 -4 0 -39 c0 -36 2 -39 33 -42 l32 -3 3 -132 3 -133
-36 0 c-34 0 -35 -1 -35 -40 l0 -40 190 0 190 0 0 40 0 39 -57 3 -58 3 -3 84
c-3 94 7 131 46 161 34 28 42 25 42 -14 0 -44 22 -61 81 -61 63 0 94 28 94 85
0 57 -37 91 -104 98 -57 5 -134 -18 -151 -46 -7 -14 -9 -10 -10 16 0 20 -5 31
-12 30 -7 -1 -66 -4 -130 -5z"/>
<path d="M2867 2370 c-65 -17 -116 -49 -144 -92 -25 -37 -28 -51 -28 -124 0
-75 3 -86 30 -125 18 -26 50 -53 80 -68 44 -23 61 -26 160 -26 101 0 115 2
168 28 40 20 66 42 85 70 24 36 27 50 27 123 0 75 -3 86 -30 125 -45 64 -108
91 -220 95 -49 2 -107 -1 -128 -6z m137 -81 c34 -16 51 -62 50 -139 -1 -108
-45 -158 -115 -130 -36 15 -54 60 -54 135 0 113 47 166 119 134z"/>
<path d="M3418 2364 c-117 -36 -155 -161 -68 -226 15 -11 78 -29 148 -43 134
-27 142 -30 142 -50 0 -18 -34 -28 -87 -27 -61 0 -109 19 -137 53 -20 23 -33
29 -65 29 l-41 0 0 -80 0 -80 44 0 c24 0 46 5 48 11 3 8 20 6 58 -5 206 -61
407 55 335 193 -19 37 -63 57 -182 81 -142 29 -143 29 -143 44 0 21 57 37 104
30 52 -9 92 -29 106 -54 8 -15 21 -20 55 -20 l45 0 0 80 0 80 -39 0 c-24 0
-41 -5 -44 -14 -5 -13 -13 -13 -59 0 -67 17 -159 17 -220 -2z"/>
<path d="M3968 2363 c-66 -22 -100 -60 -106 -121 -8 -91 25 -115 213 -152 122
-24 137 -32 113 -56 -12 -12 -34 -17 -77 -16 -68 1 -125 23 -148 58 -12 18
-24 24 -54 24 l-39 0 0 -80 0 -80 38 0 c21 0 42 4 48 10 6 6 27 4 59 -6 113
-35 279 -1 328 68 26 35 26 101 0 136 -28 38 -55 49 -188 76 -63 13 -118 26
-122 29 -12 13 7 34 36 40 51 11 127 -11 158 -44 22 -23 36 -29 70 -29 l43 0
0 80 0 80 -39 0 c-21 0 -44 -6 -50 -14 -9 -11 -19 -11 -63 0 -68 18 -161 17
-220 -3z"/>
<path d="M5025 2371 c-125 -31 -199 -135 -180 -253 20 -117 99 -176 250 -185
162 -11 272 39 300 135 6 21 4 22 -47 22 -45 0 -55 -3 -63 -22 -16 -36 -53
-49 -123 -45 -69 3 -105 27 -117 80 l-7 27 181 0 181 0 0 38 c0 51 -41 134
-80 162 -17 12 -52 29 -77 36 -48 15 -167 17 -218 5z"/>
<path d="M5558 2373 l-118 -4 0 -39 c0 -37 2 -40 28 -40 51 0 53 -7 50 -141
l-3 -124 -32 -3 c-31 -3 -33 -6 -33 -43 l0 -39 190 0 190 0 0 40 0 40 -60 0
-60 0 0 103 0 103 33 32 c41 40 55 41 49 3 -10 -72 115 -97 165 -32 22 28 16
89 -11 116 -44 43 -148 46 -206 5 l-30 -21 0 25 c0 19 -5 25 -17 24 -10 -1
-71 -4 -135 -5z"/>
<path d="M6327 2366 c-20 -8 -44 -19 -52 -26 -13 -10 -15 -9 -15 9 0 20 -4 21
-130 21 l-130 0 0 -39 c0 -37 2 -40 33 -43 l32 -3 3 -132 3 -133 -36 0 c-34 0
-35 -1 -35 -40 l0 -40 160 0 160 0 0 39 c0 36 -3 40 -27 43 l-28 3 -3 106 c-3
119 5 135 69 145 60 10 73 -10 78 -115 4 -117 -2 -141 -34 -141 -22 0 -25 -4
-25 -40 l0 -40 150 0 150 0 0 39 c0 36 -3 40 -27 43 l-28 3 0 108 c0 102 1
110 24 128 29 24 72 24 100 2 20 -17 22 -26 19 -128 l-3 -110 -27 -3 c-25 -3
-28 -7 -28 -43 l0 -39 160 0 160 0 0 40 c0 39 -1 40 -34 40 -22 0 -36 6 -40
16 -3 9 -6 67 -6 129 0 130 -8 156 -59 188 -30 18 -50 22 -122 22 -76 0 -90
-3 -126 -27 -37 -25 -43 -26 -55 -11 -34 42 -153 56 -231 29z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.8 KiB

View File

@ -1,14 +0,0 @@
# Known Problems
There are some problems I discovered during development.
And I don't think it has to do anything with crossterm but it has to do whit how terminals handle ANSI or WinApi.
## WinAPI
- Power shell does not interpreter 'DarkYellow' and is instead using gray instead, cmd is working perfectly fine.
- Power shell inserts an '\n' (enter) when the program starts, this enter is the one you pressed when running the command.
- After the program ran, power shell will reset the background and foreground colors.
## UNIX-terminals
The Arc and Manjaro KDE Konsole's are not seeming to resize the terminal instead they are resizing the buffer.

View File

@ -1,40 +0,0 @@
![Lines of Code][s7] [![MIT][s2]][l2] [![Join us on Discord][s5]][l5]
# Crossterm Examples
The examples are compatible with the latest release.
## Structure
```
├── examples
│   └── interactive-test
│   └── event-*
│   └── stderr
```
| File Name | Description | Topics |
|:----------------------------|:-------------------------------|:------------------------------------------|
| `examples/interactive-test` | interactive, walk through, demo | cursor, style, event |
| `event-*` | event reading demos | (async) event reading |
| `stderr` | crossterm over stderr demo | raw mode, alternate screen, custom output |
| `is_tty` | Is this instance a tty ? | tty |
## Run examples
```bash
$ cargo run --example [file name]
```
To run the interactive-demo go into the folder `examples/interactive-demo` and run `cargo run`.
## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details.
[s2]: https://img.shields.io/badge/license-MIT-blue.svg
[l2]: LICENSE
[s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord
[l5]: https://discord.gg/K4nyTDB
[s7]: https://travis-ci.org/crossterm-rs/examples.svg?branch=master

View File

@ -1,68 +0,0 @@
//! Demonstrates how to match on modifiers like: Control, alt, shift.
//!
//! cargo run --example event-match-modifiers
use keyfork_crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
fn match_event(read_event: Event) {
match read_event {
// Match one one modifier:
Event::Key(KeyEvent {
modifiers: KeyModifiers::CONTROL,
code,
..
}) => {
println!("Control + {:?}", code);
}
Event::Key(KeyEvent {
modifiers: KeyModifiers::SHIFT,
code,
..
}) => {
println!("Shift + {:?}", code);
}
Event::Key(KeyEvent {
modifiers: KeyModifiers::ALT,
code,
..
}) => {
println!("Alt + {:?}", code);
}
// Match on multiple modifiers:
Event::Key(KeyEvent {
code, modifiers, ..
}) => {
if modifiers == (KeyModifiers::ALT | KeyModifiers::SHIFT) {
println!("Alt + Shift {:?}", code);
} else {
println!("({:?}) with key: {:?}", modifiers, code)
}
}
_ => {}
}
}
fn main() {
match_event(Event::Key(KeyEvent::new(
KeyCode::Char('z'),
KeyModifiers::CONTROL,
)));
match_event(Event::Key(KeyEvent::new(
KeyCode::Left,
KeyModifiers::SHIFT,
)));
match_event(Event::Key(KeyEvent::new(
KeyCode::Delete,
KeyModifiers::ALT,
)));
match_event(Event::Key(KeyEvent::new(
KeyCode::Right,
KeyModifiers::ALT | KeyModifiers::SHIFT,
)));
match_event(Event::Key(KeyEvent::new(
KeyCode::Home,
KeyModifiers::ALT | KeyModifiers::CONTROL,
)));
}

View File

@ -1,61 +0,0 @@
//! Demonstrates how to match on modifiers like: Control, alt, shift.
//!
//! cargo run --example event-poll-read
use std::{io, time::Duration};
use keyfork_crossterm::{
cursor::position,
event::{poll, read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode},
};
const HELP: &str = r#"Blocking poll() & non-blocking read()
- Keyboard, mouse and terminal resize events enabled
- Prints "." every second if there's no event
- Hit "c" to print current cursor position
- Use Esc to quit
"#;
fn print_events() -> io::Result<()> {
loop {
// Wait up to 1s for another event
if poll(Duration::from_millis(1_000))? {
// It's guaranteed that read() won't block if `poll` returns `Ok(true)`
let event = read()?;
println!("Event::{:?}\r", event);
if event == Event::Key(KeyCode::Char('c').into()) {
println!("Cursor position: {:?}\r", position());
}
if event == Event::Key(KeyCode::Esc.into()) {
break;
}
} else {
// Timeout expired, no event for 1s
println!(".\r");
}
}
Ok(())
}
fn main() -> io::Result<()> {
println!("{}", HELP);
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnableMouseCapture)?;
if let Err(e) = print_events() {
println!("Error: {:?}\r", e);
}
execute!(stdout, DisableMouseCapture)?;
disable_raw_mode()
}

View File

@ -1,46 +0,0 @@
//! Demonstrates how to block read characters or a full line.
//! Just note that crossterm is not required to do this and can be done with `io::stdin()`.
//!
//! cargo run --example event-read-char-line
use std::io;
use keyfork_crossterm::event::{self, Event, KeyCode, KeyEvent};
/// Read a character from input.
pub fn read_char() -> io::Result<char> {
loop {
if let Event::Key(KeyEvent {
code: KeyCode::Char(c),
..
}) = event::read()?
{
return Ok(c);
}
}
}
/// Read a line from input.
pub fn read_line() -> io::Result<String> {
let mut line = String::new();
while let Event::Key(KeyEvent { code, .. }) = event::read()? {
match code {
KeyCode::Enter => {
break;
}
KeyCode::Char(c) => {
line.push(c);
}
_ => {}
}
}
Ok(line)
}
fn main() {
println!("read line:");
println!("{:?}", read_line());
println!("read char:");
println!("{:?}", read_char());
}

View File

@ -1,113 +0,0 @@
//! Demonstrates how to block read events.
//!
//! cargo run --example event-read
use std::io;
use keyfork_crossterm::event::{
poll, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
};
use keyfork_crossterm::{
cursor::position,
event::{
read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
EnableFocusChange, EnableMouseCapture, Event, KeyCode,
},
execute, queue,
terminal::{disable_raw_mode, enable_raw_mode},
};
use std::time::Duration;
const HELP: &str = r#"Blocking read()
- Keyboard, mouse, focus and terminal resize events enabled
- Hit "c" to print current cursor position
- Use Esc to quit
"#;
fn print_events() -> io::Result<()> {
loop {
// Blocking read
let event = read()?;
println!("Event: {:?}\r", event);
if event == Event::Key(KeyCode::Char('c').into()) {
println!("Cursor position: {:?}\r", position());
}
if let Event::Resize(x, y) = event {
let (original_size, new_size) = flush_resize_events((x, y));
println!("Resize from: {:?}, to: {:?}\r", original_size, new_size);
}
if event == Event::Key(KeyCode::Esc.into()) {
break;
}
}
Ok(())
}
// Resize events can occur in batches.
// With a simple loop they can be flushed.
// This function will keep the first and last resize event.
fn flush_resize_events(first_resize: (u16, u16)) -> ((u16, u16), (u16, u16)) {
let mut last_resize = first_resize;
while let Ok(true) = poll(Duration::from_millis(50)) {
if let Ok(Event::Resize(x, y)) = read() {
last_resize = (x, y);
}
}
(first_resize, last_resize)
}
fn main() -> io::Result<()> {
println!("{}", HELP);
enable_raw_mode()?;
let mut stdout = io::stdout();
let supports_keyboard_enhancement = matches!(
keyfork_crossterm::terminal::supports_keyboard_enhancement(),
Ok(true)
);
if supports_keyboard_enhancement {
queue!(
stdout,
PushKeyboardEnhancementFlags(
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
)
)?;
}
execute!(
stdout,
EnableBracketedPaste,
EnableFocusChange,
EnableMouseCapture,
)?;
if let Err(e) = print_events() {
println!("Error: {:?}\r", e);
}
if supports_keyboard_enhancement {
queue!(stdout, PopKeyboardEnhancementFlags)?;
}
execute!(
stdout,
DisableBracketedPaste,
PopKeyboardEnhancementFlags,
DisableFocusChange,
DisableMouseCapture
)?;
disable_raw_mode()
}

View File

@ -1,67 +0,0 @@
//! Demonstrates how to read events asynchronously with async-std.
//!
//! cargo run --features="event-stream" --example event-stream-async-std
use std::{io::stdout, time::Duration};
use futures::{future::FutureExt, select, StreamExt};
use futures_timer::Delay;
use keyfork_crossterm::{
cursor::position,
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode},
};
const HELP: &str = r#"EventStream based on futures_util::stream::Stream with async-std
- Keyboard, mouse and terminal resize events enabled
- Prints "." every second if there's no event
- Hit "c" to print current cursor position
- Use Esc to quit
"#;
async fn print_events() {
let mut reader = EventStream::new();
loop {
let mut delay = Delay::new(Duration::from_millis(1_000)).fuse();
let mut event = reader.next().fuse();
select! {
_ = delay => { println!(".\r"); },
maybe_event = event => {
match maybe_event {
Some(Ok(event)) => {
println!("Event::{:?}\r", event);
if event == Event::Key(KeyCode::Char('c').into()) {
println!("Cursor position: {:?}\r", position());
}
if event == Event::Key(KeyCode::Esc.into()) {
break;
}
}
Some(Err(e)) => println!("Error: {:?}\r", e),
None => break,
}
}
};
}
}
fn main() -> std::io::Result<()> {
println!("{}", HELP);
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnableMouseCapture)?;
async_std::task::block_on(print_events());
execute!(stdout, DisableMouseCapture)?;
disable_raw_mode()
}

View File

@ -1,68 +0,0 @@
//! Demonstrates how to read events asynchronously with tokio.
//!
//! cargo run --features="event-stream" --example event-stream-tokio
use std::{io::stdout, time::Duration};
use futures::{future::FutureExt, select, StreamExt};
use futures_timer::Delay;
use keyfork_crossterm::{
cursor::position,
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode},
execute,
terminal::{disable_raw_mode, enable_raw_mode},
};
const HELP: &str = r#"EventStream based on futures_util::Stream with tokio
- Keyboard, mouse and terminal resize events enabled
- Prints "." every second if there's no event
- Hit "c" to print current cursor position
- Use Esc to quit
"#;
async fn print_events() {
let mut reader = EventStream::new();
loop {
let mut delay = Delay::new(Duration::from_millis(1_000)).fuse();
let mut event = reader.next().fuse();
select! {
_ = delay => { println!(".\r"); },
maybe_event = event => {
match maybe_event {
Some(Ok(event)) => {
println!("Event::{:?}\r", event);
if event == Event::Key(KeyCode::Char('c').into()) {
println!("Cursor position: {:?}\r", position());
}
if event == Event::Key(KeyCode::Esc.into()) {
break;
}
}
Some(Err(e)) => println!("Error: {:?}\r", e),
None => break,
}
}
};
}
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
println!("{}", HELP);
enable_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnableMouseCapture)?;
print_events().await;
execute!(stdout, DisableMouseCapture)?;
disable_raw_mode()
}

View File

@ -1,13 +0,0 @@
[package]
name = "interactive-demo"
version = "0.0.1"
authors = ["T. Post", "Robert Vojta <rvojta@me.com>"]
edition = "2018"
description = "Interactive demo for crossterm."
license = "MIT"
exclude = ["target", "Cargo.lock"]
readme = "README.md"
publish = false
[dependencies]
crossterm = { path = "../../" }

View File

@ -1,29 +0,0 @@
macro_rules! run_tests {
(
$dst:expr,
$(
$testfn:ident
),*
$(,)?
) => {
use keyfork_crossterm::{queue, style, terminal, cursor};
$(
queue!(
$dst,
style::ResetColor,
terminal::Clear(terminal::ClearType::All),
cursor::MoveTo(1, 1),
cursor::Show,
cursor::EnableBlinking
)?;
$testfn($dst)?;
match $crate::read_char() {
Ok('q') => return Ok(()),
Err(e) => return Err(e),
_ => { },
};
)*
}
}

View File

@ -1,104 +0,0 @@
#![allow(clippy::cognitive_complexity)]
use std::io;
use keyfork_crossterm::event::KeyEventKind;
pub use keyfork_crossterm::{
cursor,
event::{self, Event, KeyCode, KeyEvent},
execute, queue, style,
terminal::{self, ClearType},
Command,
};
#[macro_use]
mod macros;
mod test;
const MENU: &str = r#"Crossterm interactive test
Controls:
- 'q' - quit interactive test (or return to this menu)
- any other key - continue with next step
Available tests:
1. cursor
2. color (foreground, background)
3. attributes (bold, italic, ...)
4. input
5. synchronized output
Select test to run ('1', '2', ...) or hit 'q' to quit.
"#;
fn run<W>(w: &mut W) -> io::Result<()>
where
W: io::Write,
{
execute!(w, terminal::EnterAlternateScreen)?;
terminal::enable_raw_mode()?;
loop {
queue!(
w,
style::ResetColor,
terminal::Clear(ClearType::All),
cursor::Hide,
cursor::MoveTo(1, 1)
)?;
for line in MENU.split('\n') {
queue!(w, style::Print(line), cursor::MoveToNextLine(1))?;
}
w.flush()?;
match read_char()? {
'1' => test::cursor::run(w)?,
'2' => test::color::run(w)?,
'3' => test::attribute::run(w)?,
'4' => test::event::run(w)?,
'5' => test::synchronized_output::run(w)?,
'q' => {
execute!(w, cursor::SetCursorStyle::DefaultUserShape).unwrap();
break;
}
_ => {}
};
}
execute!(
w,
style::ResetColor,
cursor::Show,
terminal::LeaveAlternateScreen
)?;
terminal::disable_raw_mode()
}
pub fn read_char() -> std::io::Result<char> {
loop {
if let Ok(Event::Key(KeyEvent {
code: KeyCode::Char(c),
kind: KeyEventKind::Press,
modifiers: _,
state: _,
})) = event::read()
{
return Ok(c);
}
}
}
pub fn buffer_size() -> io::Result<(u16, u16)> {
terminal::size()
}
fn main() -> std::io::Result<()> {
let mut stdout = io::stdout();
run(&mut stdout)
}

View File

@ -1,5 +0,0 @@
pub mod attribute;
pub mod color;
pub mod cursor;
pub mod event;
pub mod synchronized_output;

View File

@ -1,58 +0,0 @@
#![allow(clippy::cognitive_complexity)]
use keyfork_crossterm::{cursor, queue, style};
use std::io::Write;
const ATTRIBUTES: [(style::Attribute, style::Attribute); 10] = [
(style::Attribute::Bold, style::Attribute::NormalIntensity),
(style::Attribute::Italic, style::Attribute::NoItalic),
(style::Attribute::Underlined, style::Attribute::NoUnderline),
(
style::Attribute::DoubleUnderlined,
style::Attribute::NoUnderline,
),
(style::Attribute::Undercurled, style::Attribute::NoUnderline),
(style::Attribute::Underdotted, style::Attribute::NoUnderline),
(style::Attribute::Underdashed, style::Attribute::NoUnderline),
(style::Attribute::Reverse, style::Attribute::NoReverse),
(
style::Attribute::CrossedOut,
style::Attribute::NotCrossedOut,
),
(style::Attribute::SlowBlink, style::Attribute::NoBlink),
];
fn test_set_display_attributes<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
queue!(
w,
style::Print("Display attributes"),
cursor::MoveToNextLine(2)
)?;
for (on, off) in &ATTRIBUTES {
queue!(
w,
style::SetAttribute(*on),
style::Print(format!("{:>width$} ", format!("{:?}", on), width = 35)),
style::SetAttribute(*off),
style::Print(format!("{:>width$}", format!("{:?}", off), width = 35)),
style::ResetColor,
cursor::MoveToNextLine(1)
)?;
}
w.flush()?;
Ok(())
}
pub fn run<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
run_tests!(w, test_set_display_attributes,);
Ok(())
}

View File

@ -1,198 +0,0 @@
#![allow(clippy::cognitive_complexity)]
use keyfork_crossterm::{cursor, queue, style, style::Color};
use std::io::Write;
const COLORS: [Color; 21] = [
Color::Black,
Color::DarkGrey,
Color::Grey,
Color::White,
Color::DarkRed,
Color::Red,
Color::DarkGreen,
Color::Green,
Color::DarkYellow,
Color::Yellow,
Color::DarkBlue,
Color::Blue,
Color::DarkMagenta,
Color::Magenta,
Color::DarkCyan,
Color::Cyan,
Color::AnsiValue(0),
Color::AnsiValue(15),
Color::Rgb { r: 255, g: 0, b: 0 },
Color::Rgb { r: 0, g: 255, b: 0 },
Color::Rgb { r: 0, g: 0, b: 255 },
];
fn test_set_foreground_color<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
queue!(
w,
style::Print("Foreground colors on the black & white background"),
cursor::MoveToNextLine(2)
)?;
for color in &COLORS {
queue!(
w,
style::SetForegroundColor(*color),
style::SetBackgroundColor(Color::Black),
style::Print(format!(
"{:>width$} ",
format!("{:?} ████████████", color),
width = 40
)),
style::SetBackgroundColor(Color::White),
style::Print(format!(
"{:>width$}",
format!("{:?} ████████████", color),
width = 40
)),
cursor::MoveToNextLine(1)
)?;
}
w.flush()?;
Ok(())
}
fn test_set_background_color<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
queue!(
w,
style::Print("Background colors with black & white foreground"),
cursor::MoveToNextLine(2)
)?;
for color in &COLORS {
queue!(
w,
style::SetBackgroundColor(*color),
style::SetForegroundColor(Color::Black),
style::Print(format!(
"{:>width$} ",
format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color),
width = 40
)),
style::SetForegroundColor(Color::White),
style::Print(format!(
"{:>width$}",
format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color),
width = 40
)),
cursor::MoveToNextLine(1)
)?;
}
w.flush()?;
Ok(())
}
fn test_color_values_matrix_16x16<W, F>(w: &mut W, title: &str, color: F) -> std::io::Result<()>
where
W: Write,
F: Fn(u16, u16) -> Color,
{
queue!(w, style::Print(title))?;
for idx in 0..=15 {
queue!(
w,
cursor::MoveTo(1, idx + 4),
style::Print(format!("{:>width$}", idx, width = 2))
)?;
queue!(
w,
cursor::MoveTo(idx * 3 + 3, 3),
style::Print(format!("{:>width$}", idx, width = 3))
)?;
}
for row in 0..=15u16 {
queue!(w, cursor::MoveTo(4, row + 4))?;
for col in 0..=15u16 {
queue!(
w,
style::SetForegroundColor(color(col, row)),
style::Print("███")
)?;
}
queue!(
w,
style::SetForegroundColor(Color::White),
style::Print(format!("{:>width$} ..= ", row * 16, width = 3)),
style::Print(format!("{:>width$}", row * 16 + 15, width = 3))
)?;
}
w.flush()?;
Ok(())
}
fn test_color_ansi_values<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
test_color_values_matrix_16x16(w, "Color::Ansi values", |col, row| {
Color::AnsiValue((row * 16 + col) as u8)
})
}
fn test_rgb_red_values<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
test_color_values_matrix_16x16(w, "Color::Rgb red values", |col, row| Color::Rgb {
r: (row * 16 + col) as u8,
g: 0_u8,
b: 0,
})
}
fn test_rgb_green_values<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
test_color_values_matrix_16x16(w, "Color::Rgb green values", |col, row| Color::Rgb {
r: 0,
g: (row * 16 + col) as u8,
b: 0,
})
}
fn test_rgb_blue_values<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
test_color_values_matrix_16x16(w, "Color::Rgb blue values", |col, row| Color::Rgb {
r: 0,
g: 0,
b: (row * 16 + col) as u8,
})
}
pub fn run<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
run_tests!(
w,
test_set_foreground_color,
test_set_background_color,
test_color_ansi_values,
test_rgb_red_values,
test_rgb_green_values,
test_rgb_blue_values,
);
Ok(())
}

View File

@ -1,222 +0,0 @@
#![allow(clippy::cognitive_complexity)]
use std::io::Write;
use keyfork_crossterm::{cursor, execute, queue, style, style::Stylize, Command};
use std::thread;
use std::time::Duration;
fn test_move_cursor_up<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
draw_cursor_box(w, "Move Up (2)", |_, _| cursor::MoveUp(2))
}
fn test_move_cursor_down<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
draw_cursor_box(w, "Move Down (2)", |_, _| cursor::MoveDown(2))
}
fn test_move_cursor_left<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
draw_cursor_box(w, "Move Left (2)", |_, _| cursor::MoveLeft(2))
}
fn test_move_cursor_right<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
draw_cursor_box(w, "Move Right (2)", |_, _| cursor::MoveRight(2))
}
fn test_move_cursor_to_previous_line<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
draw_cursor_box(w, "MoveToPreviousLine (1)", |_, _| {
cursor::MoveToPreviousLine(1)
})
}
fn test_move_cursor_to_next_line<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
draw_cursor_box(w, "MoveToNextLine (1)", |_, _| cursor::MoveToNextLine(1))
}
fn test_move_cursor_to_column<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
draw_cursor_box(w, "MoveToColumn (1)", |center_x, _| {
cursor::MoveToColumn(center_x + 1)
})
}
fn test_hide_cursor<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
execute!(w, style::Print("HideCursor"), cursor::Hide)
}
fn test_show_cursor<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
execute!(w, style::Print("ShowCursor"), cursor::Show)
}
fn test_cursor_blinking_block<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
execute!(
w,
style::Print("Blinking Block:"),
cursor::MoveLeft(2),
cursor::SetCursorStyle::BlinkingBlock,
)
}
fn test_cursor_blinking_underscore<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
execute!(
w,
style::Print("Blinking Underscore:"),
cursor::MoveLeft(2),
cursor::SetCursorStyle::BlinkingUnderScore,
)
}
fn test_cursor_blinking_bar<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
execute!(
w,
style::Print("Blinking bar:"),
cursor::MoveLeft(2),
cursor::SetCursorStyle::BlinkingBar,
)
}
fn test_move_cursor_to<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
draw_cursor_box(
w,
"MoveTo (x: 1, y: 1) removed from center",
|center_x, center_y| cursor::MoveTo(center_x + 1, center_y + 1),
)
}
fn test_save_restore_cursor_position<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
execute!(w,
cursor::MoveTo(0, 0),
style::Print("Save position, print character elsewhere, after three seconds restore to old position."),
cursor::MoveToNextLine(2),
style::Print("Save ->[ ]<- Position"),
cursor::MoveTo(8, 2),
cursor::SavePosition,
cursor::MoveTo(10,10),
style::Print("Move To ->[√]<- Position")
)?;
thread::sleep(Duration::from_secs(3));
execute!(w, cursor::RestorePosition, style::Print(""))
}
/// Draws a box with an colored center, this center can be taken as a reference point after running the given cursor command.
fn draw_cursor_box<W, F, T>(w: &mut W, description: &str, cursor_command: F) -> std::io::Result<()>
where
W: Write,
F: Fn(u16, u16) -> T,
T: Command,
{
execute!(
w,
cursor::Hide,
cursor::MoveTo(0, 0),
style::SetForegroundColor(style::Color::Red),
style::Print(format!(
"Red box is the center. After the action: '{}' '√' is drawn to reflect the action from the center.",
description
))
)?;
let start_y = 2;
let width = 21;
let height = 11 + start_y;
let center_x = width / 2;
let center_y = (height + start_y) / 2;
for row in start_y..=10 + start_y {
for column in 0..=width {
if (row == start_y || row == height - 1) || (column == 0 || column == width) {
queue!(
w,
cursor::MoveTo(column, row),
style::PrintStyledContent("".red()),
)?;
} else {
queue!(
w,
cursor::MoveTo(column, row),
style::PrintStyledContent("_".red().on_white())
)?;
}
}
}
queue!(
w,
cursor::MoveTo(center_x, center_y),
style::PrintStyledContent("".red().on_white()),
cursor::MoveTo(center_x, center_y),
)?;
queue!(
w,
cursor_command(center_x, center_y),
style::PrintStyledContent("".magenta().on_white())
)?;
w.flush()?;
Ok(())
}
pub fn run<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
run_tests!(
w,
test_hide_cursor,
test_show_cursor,
test_cursor_blinking_bar,
test_cursor_blinking_block,
test_cursor_blinking_underscore,
test_move_cursor_left,
test_move_cursor_right,
test_move_cursor_up,
test_move_cursor_down,
test_move_cursor_to,
test_move_cursor_to_next_line,
test_move_cursor_to_previous_line,
test_move_cursor_to_column,
test_save_restore_cursor_position
);
Ok(())
}

View File

@ -1,42 +0,0 @@
#![allow(clippy::cognitive_complexity)]
use keyfork_crossterm::{
cursor::position,
event::{read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
};
use std::io::{self, Write};
fn test_event<W>(w: &mut W) -> io::Result<()>
where
W: io::Write,
{
execute!(w, EnableMouseCapture)?;
loop {
// Blocking read
let event = read()?;
println!("Event::{:?}\r", event);
if event == Event::Key(KeyCode::Char('c').into()) {
println!("Cursor position: {:?}\r", position());
}
if event == Event::Key(KeyCode::Char('q').into()) {
break;
}
}
execute!(w, DisableMouseCapture)?;
Ok(())
}
pub fn run<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
run_tests!(w, test_event);
Ok(())
}

View File

@ -1,41 +0,0 @@
use std::io::Write;
use keyfork_crossterm::{cursor, execute, style::Print, SynchronizedUpdate};
fn render_slowly<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
for i in 1..10 {
execute!(w, Print(format!("{}", i)))?;
std::thread::sleep(std::time::Duration::from_millis(50));
}
Ok(())
}
fn test_slow_rendering<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
execute!(w, Print("Rendering without synchronized update:"))?;
execute!(w, cursor::MoveToNextLine(1))?;
std::thread::sleep(std::time::Duration::from_millis(50));
render_slowly(w)?;
execute!(w, cursor::MoveToNextLine(1))?;
execute!(w, Print("Rendering with synchronized update:"))?;
execute!(w, cursor::MoveToNextLine(1))?;
std::thread::sleep(std::time::Duration::from_millis(50));
w.sync_update(render_slowly)??;
execute!(w, cursor::MoveToNextLine(1))?;
Ok(())
}
pub fn run<W>(w: &mut W) -> std::io::Result<()>
where
W: Write,
{
run_tests!(w, test_slow_rendering,);
Ok(())
}

View File

@ -1,20 +0,0 @@
#![allow(missing_docs)]
use keyfork_crossterm::{
execute,
terminal::{size, SetSize},
tty::IsTty,
};
use std::io::{stdin, stdout};
fn main() {
println!("size: {:?}", size().unwrap());
execute!(stdout(), SetSize(10, 10)).unwrap();
println!("resized: {:?}", size().unwrap());
if stdin().is_tty() {
println!("Is TTY");
} else {
println!("Is not TTY");
}
}

View File

@ -1,96 +0,0 @@
//! This shows how an application can write on stderr
//! instead of stdout, thus making it possible to
//! the command API instead of the "old style" direct
//! unbuffered API.
//!
//! This particular example is only suited to Unix
//! for now.
//!
//! cargo run --example stderr
use std::io;
use keyfork_crossterm::{
cursor::{Hide, MoveTo, Show},
event,
event::{Event, KeyCode, KeyEvent},
execute, queue,
style::Print,
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
};
const TEXT: &str = r#"
This screen is ran on stderr.
And when you hit enter, it prints on stdout.
This makes it possible to run an application and choose what will
be sent to any application calling yours.
For example, assuming you build this example with
cargo build --bin stderr
and then you run it with
cd "$(target/debug/stderr)"
what the application prints on stdout is used as argument to cd.
Try it out.
Hit any key to quit this screen:
1 will print `..`
2 will print `/`
3 will print `~`
Any other key will print this text (so that you may copy-paste)
"#;
fn run_app<W>(write: &mut W) -> io::Result<char>
where
W: io::Write,
{
queue!(
write,
EnterAlternateScreen, // enter alternate screen
Hide // hide the cursor
)?;
let mut y = 1;
for line in TEXT.split('\n') {
queue!(write, MoveTo(1, y), Print(line.to_string()))?;
y += 1;
}
write.flush()?;
terminal::enable_raw_mode()?;
let user_char = read_char()?; // we wait for the user to hit a key
execute!(write, Show, LeaveAlternateScreen)?; // restore the cursor and leave the alternate screen
terminal::disable_raw_mode()?;
Ok(user_char)
}
/// Read a character from input.
pub fn read_char() -> io::Result<char> {
loop {
if let Event::Key(KeyEvent {
code: KeyCode::Char(c),
..
}) = event::read()?
{
return Ok(c);
}
}
}
// cargo run --example stderr
fn main() {
match run_app(&mut io::stderr()).unwrap() {
'1' => print!(".."),
'2' => print!("/"),
'3' => print!("~"),
_ => println!("{}", TEXT),
}
}

View File

@ -1,46 +0,0 @@
use std::sync::atomic::{AtomicBool, Ordering};
use crossterm_winapi::{ConsoleMode, Handle};
use parking_lot::Once;
use winapi::um::wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING;
/// Enable virtual terminal processing.
///
/// This method attempts to enable virtual terminal processing for this
/// console. If there was a problem enabling it, then an error returned.
/// On success, the caller may assume that enabling it was successful.
///
/// When virtual terminal processing is enabled, characters emitted to the
/// console are parsed for VT100 and similar control character sequences
/// that control color and other similar operations.
fn enable_vt_processing() -> std::io::Result<()> {
let mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING;
let console_mode = ConsoleMode::from(Handle::current_out_handle()?);
let old_mode = console_mode.mode()?;
if old_mode & mask == 0 {
console_mode.set_mode(old_mode | mask)?;
}
Ok(())
}
static SUPPORTS_ANSI_ESCAPE_CODES: AtomicBool = AtomicBool::new(false);
static INITIALIZER: Once = Once::new();
/// Checks if the current terminal supports ANSI escape sequences
pub fn supports_ansi() -> bool {
INITIALIZER.call_once(|| {
// Some terminals on Windows like GitBash can't use WinAPI calls directly
// so when we try to enable the ANSI-flag for Windows this won't work.
// Because of that we should check first if the TERM-variable is set
// and see if the current terminal is a terminal who does support ANSI.
let supported = enable_vt_processing().is_ok()
|| std::env::var("TERM").map_or(false, |term| term != "dumb");
SUPPORTS_ANSI_ESCAPE_CODES.store(supported, Ordering::SeqCst);
});
SUPPORTS_ANSI_ESCAPE_CODES.load(Ordering::SeqCst)
}

View File

@ -1,295 +0,0 @@
use std::fmt;
use std::io::{self, Write};
use crate::terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate};
/// An interface for a command that performs an action on the terminal.
///
/// Crossterm provides a set of commands,
/// and there is no immediate reason to implement a command yourself.
/// In order to understand how to use and execute commands,
/// it is recommended that you take a look at [Command API](./index.html#command-api) chapter.
pub trait Command {
/// Write an ANSI representation of this command to the given writer.
/// An ANSI code can manipulate the terminal by writing it to the terminal buffer.
/// However, only Windows 10 and UNIX systems support this.
///
/// This method does not need to be accessed manually, as it is used by the crossterm's [Command API](./index.html#command-api)
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result;
/// Execute this command.
///
/// Windows versions lower than windows 10 do not support ANSI escape codes,
/// therefore a direct WinAPI call is made.
///
/// This method does not need to be accessed manually, as it is used by the crossterm's [Command API](./index.html#command-api)
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()>;
/// Returns whether the ANSI code representation of this command is supported by windows.
///
/// A list of supported ANSI escape codes
/// can be found [here](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences).
#[cfg(windows)]
fn is_ansi_code_supported(&self) -> bool {
super::ansi_support::supports_ansi()
}
}
impl<T: Command + ?Sized> Command for &T {
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
(**self).write_ansi(f)
}
#[inline]
#[cfg(windows)]
fn execute_winapi(&self) -> io::Result<()> {
T::execute_winapi(self)
}
#[cfg(windows)]
#[inline]
fn is_ansi_code_supported(&self) -> bool {
T::is_ansi_code_supported(self)
}
}
/// An interface for types that can queue commands for further execution.
pub trait QueueableCommand {
/// Queues the given command for further execution.
fn queue(&mut self, command: impl Command) -> io::Result<&mut Self>;
}
/// An interface for types that can directly execute commands.
pub trait ExecutableCommand {
/// Executes the given command directly.
fn execute(&mut self, command: impl Command) -> io::Result<&mut Self>;
}
impl<T: Write + ?Sized> QueueableCommand for T {
/// Queues the given command for further execution.
///
/// Queued commands will be executed in the following cases:
///
/// * When `flush` is called manually on the given type implementing `io::Write`.
/// * The terminal will `flush` automatically if the buffer is full.
/// * Each line is flushed in case of `stdout`, because it is line buffered.
///
/// # Arguments
///
/// - [Command](./trait.Command.html)
///
/// The command that you want to queue for later execution.
///
/// # Examples
///
/// ```rust
/// use std::io::{self, Write};
/// use keyfork_crossterm::{QueueableCommand, style::Print};
///
/// fn main() -> io::Result<()> {
/// let mut stdout = io::stdout();
///
/// // `Print` will executed executed when `flush` is called.
/// stdout
/// .queue(Print("foo 1\n".to_string()))?
/// .queue(Print("foo 2".to_string()))?;
///
/// // some other code (no execution happening here) ...
///
/// // when calling `flush` on `stdout`, all commands will be written to the stdout and therefore executed.
/// stdout.flush()?;
///
/// Ok(())
///
/// // ==== Output ====
/// // foo 1
/// // foo 2
/// }
/// ```
///
/// Have a look over at the [Command API](./index.html#command-api) for more details.
///
/// # Notes
///
/// * In the case of UNIX and Windows 10, ANSI codes are written to the given 'writer'.
/// * In case of Windows versions lower than 10, a direct WinAPI call will be made.
/// The reason for this is that Windows versions lower than 10 do not support ANSI codes,
/// and can therefore not be written to the given `writer`.
/// Therefore, there is no difference between [execute](./trait.ExecutableCommand.html)
/// and [queue](./trait.QueueableCommand.html) for those old Windows versions.
fn queue(&mut self, command: impl Command) -> io::Result<&mut Self> {
#[cfg(windows)]
if !command.is_ansi_code_supported() {
// There may be queued commands in this writer, but `execute_winapi` will execute the
// command immediately. To prevent commands being executed out of order we flush the
// writer now.
self.flush()?;
command.execute_winapi()?;
return Ok(self);
}
write_command_ansi(self, command)?;
Ok(self)
}
}
impl<T: Write + ?Sized> ExecutableCommand for T {
/// Executes the given command directly.
///
/// The given command its ANSI escape code will be written and flushed onto `Self`.
///
/// # Arguments
///
/// - [Command](./trait.Command.html)
///
/// The command that you want to execute directly.
///
/// # Example
///
/// ```rust
/// use std::io;
/// use keyfork_crossterm::{ExecutableCommand, style::Print};
///
/// fn main() -> io::Result<()> {
/// // will be executed directly
/// io::stdout()
/// .execute(Print("sum:\n".to_string()))?
/// .execute(Print(format!("1 + 1= {} ", 1 + 1)))?;
///
/// Ok(())
///
/// // ==== Output ====
/// // sum:
/// // 1 + 1 = 2
/// }
/// ```
///
/// Have a look over at the [Command API](./index.html#command-api) for more details.
///
/// # Notes
///
/// * In the case of UNIX and Windows 10, ANSI codes are written to the given 'writer'.
/// * In case of Windows versions lower than 10, a direct WinAPI call will be made.
/// The reason for this is that Windows versions lower than 10 do not support ANSI codes,
/// and can therefore not be written to the given `writer`.
/// Therefore, there is no difference between [execute](./trait.ExecutableCommand.html)
/// and [queue](./trait.QueueableCommand.html) for those old Windows versions.
fn execute(&mut self, command: impl Command) -> io::Result<&mut Self> {
self.queue(command)?;
self.flush()?;
Ok(self)
}
}
/// An interface for types that support synchronized updates.
pub trait SynchronizedUpdate {
/// Performs a set of actions against the given type.
fn sync_update<T>(&mut self, operations: impl FnOnce(&mut Self) -> T) -> io::Result<T>;
}
impl<W: std::io::Write + ?Sized> SynchronizedUpdate for W {
/// Performs a set of actions within a synchronous update.
///
/// Updates will be suspended in the terminal, the function will be executed against self,
/// updates will be resumed, and a flush will be performed.
///
/// # Arguments
///
/// - Function
///
/// A function that performs the operations that must execute in a synchronized update.
///
/// # Examples
///
/// ```rust
/// use std::io;
/// use keyfork_crossterm::{ExecutableCommand, SynchronizedUpdate, style::Print};
///
/// fn main() -> io::Result<()> {
/// let mut stdout = io::stdout();
///
/// stdout.sync_update(|stdout| {
/// stdout.execute(Print("foo 1\n".to_string()))?;
/// stdout.execute(Print("foo 2".to_string()))?;
/// // The effects of the print command will not be present in the terminal
/// // buffer, but not visible in the terminal.
/// std::io::Result::Ok(())
/// })?;
///
/// // The effects of the commands will be visible.
///
/// Ok(())
///
/// // ==== Output ====
/// // foo 1
/// // foo 2
/// }
/// ```
///
/// # Notes
///
/// This command is performed only using ANSI codes, and will do nothing on terminals that do not support ANSI
/// codes, or this specific extension.
///
/// When rendering the screen of the terminal, the Emulator usually iterates through each visible grid cell and
/// renders its current state. With applications updating the screen a at higher frequency this can cause tearing.
///
/// This mode attempts to mitigate that.
///
/// When the synchronization mode is enabled following render calls will keep rendering the last rendered state.
/// The terminal Emulator keeps processing incoming text and sequences. When the synchronized update mode is disabled
/// again the renderer may fetch the latest screen buffer state again, effectively avoiding the tearing effect
/// by unintentionally rendering in the middle a of an application screen update.
///
fn sync_update<T>(&mut self, operations: impl FnOnce(&mut Self) -> T) -> io::Result<T> {
self.queue(BeginSynchronizedUpdate)?;
let result = operations(self);
self.execute(EndSynchronizedUpdate)?;
Ok(result)
}
}
/// Writes the ANSI representation of a command to the given writer.
fn write_command_ansi<C: Command>(
io: &mut (impl io::Write + ?Sized),
command: C,
) -> io::Result<()> {
struct Adapter<T> {
inner: T,
res: io::Result<()>,
}
impl<T: Write> fmt::Write for Adapter<T> {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.inner.write_all(s.as_bytes()).map_err(|e| {
self.res = Err(e);
fmt::Error
})
}
}
let mut adapter = Adapter {
inner: io,
res: Ok(()),
};
command
.write_ansi(&mut adapter)
.map_err(|fmt::Error| match adapter.res {
Ok(()) => panic!(
"<{}>::write_ansi incorrectly errored",
std::any::type_name::<C>()
),
Err(e) => e,
})
}
/// Executes the ANSI representation of a command, using the given `fmt::Write`.
pub(crate) fn execute_fmt(f: &mut impl fmt::Write, command: impl Command) -> fmt::Result {
#[cfg(windows)]
if !command.is_ansi_code_supported() {
return command.execute_winapi().map_err(|_| fmt::Error);
}
command.write_ansi(f)
}

Some files were not shown because too many files have changed in this diff Show More