Compare commits
1 Commits
main
...
anton/add-
Author | SHA1 | Date |
---|---|---|
|
898a5702bb |
|
@ -1942,7 +1942,6 @@ dependencies = [
|
||||||
name = "keyfork-qrcode"
|
name = "keyfork-qrcode"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
|
||||||
"image",
|
"image",
|
||||||
"keyfork-bug",
|
"keyfork-bug",
|
||||||
"keyfork-zbar",
|
"keyfork-zbar",
|
||||||
|
@ -2908,9 +2907,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rqrr"
|
name = "rqrr"
|
||||||
version = "0.9.0"
|
version = "0.7.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f126a9b02152815d84315316e7a759ee18a216d057095d56d19cec68a428b385"
|
checksum = "ad0cd0432e6beb2f86aa4c8af1bb5edcf3c9bcb9d4836facc048664205458575"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"g2p",
|
"g2p",
|
||||||
"image",
|
"image",
|
||||||
|
|
14
README.md
14
README.md
|
@ -123,25 +123,31 @@ To follow these steps please install [git-lfs][gl] and [git-sig][gs].
|
||||||
```
|
```
|
||||||
-->
|
-->
|
||||||
|
|
||||||
1. Clone repo
|
1. Install required dependencies
|
||||||
|
|
||||||
|
### Debian
|
||||||
|
|
||||||
|
`sudo apt install build-essential gcc clang llvm pkg-config nettle-dev libpcsclite-dev`
|
||||||
|
|
||||||
|
2. Clone repo
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git clone https://git.distrust.co/public/stack
|
git clone https://git.distrust.co/public/stack
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Verify Git signatures
|
3. Verify Git signatures
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
git verify-commit HEAD
|
git verify-commit HEAD
|
||||||
```
|
```
|
||||||
|
|
||||||
3. Install binary
|
4. Install binary
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo install --path crates/keyfork
|
cargo install --path crates/keyfork
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Optionally, build binary for distribution
|
5. Optionally, build binary for distribution
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo build --release --bin keyfork
|
cargo build --release --bin keyfork
|
||||||
|
|
|
@ -2,10 +2,9 @@
|
||||||
#![allow(clippy::expect_fun_call)]
|
#![allow(clippy::expect_fun_call)]
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
io::{Read, Write},
|
io::{stdin, stdout, Read, Write},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
str::FromStr,
|
sync::Mutex,
|
||||||
sync::{LazyLock, Mutex},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
|
@ -23,7 +22,7 @@ use keyfork_prompt::{
|
||||||
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
|
mnemonic::{MnemonicSetValidator, MnemonicValidator, WordLength},
|
||||||
Validator,
|
Validator,
|
||||||
},
|
},
|
||||||
Message as PromptMessage, PromptHandler,
|
Message as PromptMessage, PromptHandler, Terminal,
|
||||||
};
|
};
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
@ -35,30 +34,6 @@ const PLAINTEXT_LENGTH: u8 = 32 // shard
|
||||||
+ 1; // length;
|
+ 1; // length;
|
||||||
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
|
const ENCRYPTED_LENGTH: u8 = PLAINTEXT_LENGTH + 16;
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Clone, Copy)]
|
|
||||||
enum RetryScanMnemonic {
|
|
||||||
Retry,
|
|
||||||
Continue,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl keyfork_prompt::Choice for RetryScanMnemonic {
|
|
||||||
fn identifier(&self) -> Option<char> {
|
|
||||||
Some(match self {
|
|
||||||
RetryScanMnemonic::Retry => 'r',
|
|
||||||
RetryScanMnemonic::Continue => 'c',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for RetryScanMnemonic {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
RetryScanMnemonic::Retry => write!(f, "Retry scanning mnemonic."),
|
|
||||||
RetryScanMnemonic::Continue => write!(f, "Continue to manual mnemonic entry."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "openpgp")]
|
#[cfg(feature = "openpgp")]
|
||||||
pub mod openpgp;
|
pub mod openpgp;
|
||||||
|
|
||||||
|
@ -272,29 +247,20 @@ pub trait Format {
|
||||||
.lock()
|
.lock()
|
||||||
.expect(bug!(POISONED_MUTEX))
|
.expect(bug!(POISONED_MUTEX))
|
||||||
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||||
loop {
|
if let Ok(Some(qrcode_content)) =
|
||||||
if let Ok(Some(qrcode_content)) = keyfork_qrcode::scan_camera(
|
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
|
||||||
std::time::Duration::from_secs(*QRCODE_TIMEOUT),
|
{
|
||||||
0,
|
|
||||||
) {
|
|
||||||
let decoded_data = BASE64_STANDARD
|
let decoded_data = BASE64_STANDARD
|
||||||
.decode(qrcode_content)
|
.decode(qrcode_content)
|
||||||
.expect(bug!("qrcode should contain base64 encoded data"));
|
.expect(bug!("qrcode should contain base64 encoded data"));
|
||||||
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?);
|
pubkey_data = Some(decoded_data.try_into().map_err(|_| InvalidData)?)
|
||||||
break;
|
|
||||||
} else {
|
} else {
|
||||||
let mut prompt = prompt.lock().expect(bug!(POISONED_MUTEX));
|
prompt
|
||||||
let choice = keyfork_prompt::prompt_choice(
|
.lock()
|
||||||
&mut **prompt,
|
.expect(bug!(POISONED_MUTEX))
|
||||||
"A QR code could not be scanned. Retry or continue?",
|
.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
||||||
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
|
|
||||||
)?;
|
|
||||||
if choice == RetryScanMnemonic::Continue {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
||||||
let their_pubkey = match pubkey_data {
|
let their_pubkey = match pubkey_data {
|
||||||
|
@ -493,13 +459,9 @@ pub(crate) const HUNK_VERSION: u8 = 2;
|
||||||
pub(crate) const HUNK_OFFSET: usize = 2;
|
pub(crate) const HUNK_OFFSET: usize = 2;
|
||||||
|
|
||||||
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
|
const QRCODE_PROMPT: &str = "Press enter, then present QR code to camera.";
|
||||||
|
const QRCODE_TIMEOUT: u64 = 60; // One minute
|
||||||
const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: ";
|
const QRCODE_COULDNT_READ: &str = "A QR code could not be scanned. Please enter their words: ";
|
||||||
static QRCODE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
|
const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry.";
|
||||||
std::env::var("KEYFORK_QRCODE_TIMEOUT")
|
|
||||||
.ok()
|
|
||||||
.and_then(|t| u64::from_str(&t).ok())
|
|
||||||
.unwrap_or(60)
|
|
||||||
});
|
|
||||||
|
|
||||||
/// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the
|
/// Establish ECDH transport for remote operators, receive transport-encrypted shares, decrypt the
|
||||||
/// shares, and combine them.
|
/// shares, and combine them.
|
||||||
|
@ -514,7 +476,7 @@ static QRCODE_TIMEOUT: LazyLock<u64> = LazyLock::new(|| {
|
||||||
/// The function may panic if it is given payloads generated using a version of Keyfork that is
|
/// The function may panic if it is given payloads generated using a version of Keyfork that is
|
||||||
/// incompatible with the currently running version.
|
/// incompatible with the currently running version.
|
||||||
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut pm = keyfork_prompt::default_handler()?;
|
let mut pm = Terminal::new(stdin(), stdout())?;
|
||||||
|
|
||||||
let mut iter_count = None;
|
let mut iter_count = None;
|
||||||
let mut shares = vec![];
|
let mut shares = vec![];
|
||||||
|
@ -561,11 +523,9 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
#[cfg(feature = "qrcode")]
|
#[cfg(feature = "qrcode")]
|
||||||
{
|
{
|
||||||
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||||
loop {
|
if let Ok(Some(qrcode_content)) =
|
||||||
if let Ok(Some(qrcode_content)) = keyfork_qrcode::scan_camera(
|
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 0)
|
||||||
std::time::Duration::from_secs(*QRCODE_TIMEOUT),
|
{
|
||||||
0,
|
|
||||||
) {
|
|
||||||
let decoded_data = BASE64_STANDARD
|
let decoded_data = BASE64_STANDARD
|
||||||
.decode(qrcode_content)
|
.decode(qrcode_content)
|
||||||
.expect(bug!("qrcode should contain base64 encoded data"));
|
.expect(bug!("qrcode should contain base64 encoded data"));
|
||||||
|
@ -575,21 +535,12 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
ENCRYPTED_LENGTH as usize + 32,
|
ENCRYPTED_LENGTH as usize + 32,
|
||||||
bug!("invalid payload data")
|
bug!("invalid payload data")
|
||||||
);
|
);
|
||||||
let _ =
|
let _ = pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
||||||
pubkey_data.insert(decoded_data[..32].try_into().map_err(|_| InvalidData)?);
|
|
||||||
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
let _ = payload_data.insert(decoded_data[32..].to_vec());
|
||||||
} else {
|
} else {
|
||||||
let choice = keyfork_prompt::prompt_choice(
|
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
||||||
&mut *pm,
|
|
||||||
"A QR code could not be scanned. Retry or continue?",
|
|
||||||
&[RetryScanMnemonic::Retry, RetryScanMnemonic::Continue],
|
|
||||||
)?;
|
|
||||||
if choice == RetryScanMnemonic::Continue {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let (pubkey, payload) = match (pubkey_data, payload_data) {
|
let (pubkey, payload) = match (pubkey_data, payload_data) {
|
||||||
(Some(pubkey), Some(payload)) => (pubkey, payload),
|
(Some(pubkey), Some(payload)) => (pubkey, payload),
|
||||||
|
@ -599,7 +550,7 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
};
|
};
|
||||||
|
|
||||||
let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>(
|
let [pubkey_mnemonic, payload_mnemonic] = prompt_validated_wordlist::<English, _>(
|
||||||
&mut *pm,
|
&mut pm,
|
||||||
QRCODE_COULDNT_READ,
|
QRCODE_COULDNT_READ,
|
||||||
3,
|
3,
|
||||||
&*validator.to_fn(),
|
&*validator.to_fn(),
|
||||||
|
|
|
@ -87,7 +87,6 @@ impl From<&SeedSize> for usize {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, thiserror::Error)]
|
#[derive(Clone, Debug, thiserror::Error)]
|
||||||
pub enum MnemonicSeedSourceParseError {
|
pub enum MnemonicSeedSourceParseError {
|
||||||
#[error("Expected one of system, playing, tarot, dice")]
|
#[error("Expected one of system, playing, tarot, dice")]
|
||||||
|
@ -144,18 +143,6 @@ impl MnemonicSeedSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An error occurred while performing an operation.
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
pub enum Error {
|
|
||||||
/// An error occurred when interacting iwth a file.
|
|
||||||
#[error("Error while performing IO operation on: {1}")]
|
|
||||||
IOContext(#[source] std::io::Error, PathBuf),
|
|
||||||
}
|
|
||||||
|
|
||||||
fn context_stub<'a>(path: &'a Path) -> impl Fn(std::io::Error) -> Error + 'a {
|
|
||||||
|e| Error::IOContext(e, path.to_path_buf())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand, Clone, Debug)]
|
#[derive(Subcommand, Clone, Debug)]
|
||||||
pub enum MnemonicSubcommands {
|
pub enum MnemonicSubcommands {
|
||||||
/// Generate a mnemonic using a given entropy source.
|
/// Generate a mnemonic using a given entropy source.
|
||||||
|
@ -219,23 +206,19 @@ pub enum MnemonicSubcommands {
|
||||||
/// `--provision openpgp-card` or another relevant provisioner, to ensure the newly
|
/// `--provision openpgp-card` or another relevant provisioner, to ensure the newly
|
||||||
/// generated mnemonic would be decryptable by some form of provisioned hardware.
|
/// generated mnemonic would be decryptable by some form of provisioned hardware.
|
||||||
///
|
///
|
||||||
/// When given arguments in the format `--encrypt-to-self encrypted.asc,output=cert.asc`,
|
/// When given arguments in the format `--encrypt-to-self output.asc,output=encrypted.asc`,
|
||||||
/// the output of the OpenPGP certificate will be written to `cert.asc`, while the output
|
/// the output of the OpenPGP certificate will be written to `output.asc`, while the output
|
||||||
/// of the encryption will be written to `encrypted.asc`. Otherwise, the
|
/// of the encryption will be written to `encrypted.asc`. Otherwise, the
|
||||||
/// default behavior is to write the certificate to a file named after the certificate's
|
/// default behavior is to write the encrypted mnemonic to `output.enc.asc`. If either
|
||||||
/// fingerprint. If either output file already exists, it will not be overwritten, and the
|
/// output file already exists, it will not be overwritten, and the command will exit
|
||||||
/// command will exit unsuccessfully. This functionality must happen regardless if a
|
/// unsuccessfully.
|
||||||
/// provisioner output is specified, as the certificate is then used to encrypt the
|
|
||||||
/// mnemonic.
|
|
||||||
///
|
///
|
||||||
/// Additionally, when given the `account=` option (which must match the `account=` option
|
/// Additionally, when given the `account=` option (which must match the `account=` option
|
||||||
/// of the relevant provisioner), the given account will be used instead of the default
|
/// of the relevant provisioner), the given account will be used instead of the default
|
||||||
/// account of 0.
|
/// account of 0.
|
||||||
///
|
///
|
||||||
/// Because a new OpenPGP cert needs to be created, a User ID can also be supplied, using
|
/// Because a new OpenPGP key needs to be created, a User ID can also be supplied, using
|
||||||
/// the option `userid=<your User ID>`. It can contain any characters that are not a comma.
|
/// the option `userid=<your User ID>`. It can contain any characters that are not a comma.
|
||||||
/// If any other operation generating an OpenPGP key has a `userid=` field, and this
|
|
||||||
/// operation doesn't, that User ID will be used instead.
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
encrypt_to_self: Option<ValueWithOptions<PathBuf>>,
|
encrypt_to_self: Option<ValueWithOptions<PathBuf>>,
|
||||||
|
|
||||||
|
@ -245,17 +228,6 @@ pub enum MnemonicSubcommands {
|
||||||
/// Additional arguments, such as the amount of hardware to provision and the
|
/// Additional arguments, such as the amount of hardware to provision and the
|
||||||
/// account to use when deriving, can be specified by using (for example)
|
/// account to use when deriving, can be specified by using (for example)
|
||||||
/// `--provision openpgp-card,count=2,account=1`.
|
/// `--provision openpgp-card,count=2,account=1`.
|
||||||
///
|
|
||||||
/// Provisioners may output their public key, if necessary. The file path may be chosen
|
|
||||||
/// based on the provided `output` field, or automatically determined based on the content
|
|
||||||
/// of the key, such as an OpenPGP fingerprint or a public key hash. If automatically
|
|
||||||
/// generated, the filename will be printed.
|
|
||||||
///
|
|
||||||
/// If the OpenPGP Card provisioner is selected, because a new OpenPGP cert needs to be
|
|
||||||
/// created, a User ID can also be supplied, using the option `userid=<your User ID>`. It
|
|
||||||
/// can contain any characters that are not a comma. If any other operation generating an
|
|
||||||
/// OpenPGP key has a `userid=` field, and this operation doesn't, that User ID will be
|
|
||||||
/// used instead.
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
provision: Option<ValueWithOptions<provision::Provisioner>>,
|
provision: Option<ValueWithOptions<provision::Provisioner>>,
|
||||||
},
|
},
|
||||||
|
@ -324,7 +296,7 @@ fn do_encrypt_to(
|
||||||
literal_message.write_all(b"\n")?;
|
literal_message.write_all(b"\n")?;
|
||||||
literal_message.finalize()?;
|
literal_message.finalize()?;
|
||||||
|
|
||||||
let mut file = File::create_new(&output_file).map_err(context_stub(&output_file))?;
|
let mut file = File::create_new(&output_file)?;
|
||||||
if is_armored {
|
if is_armored {
|
||||||
let mut writer = Writer::new(file, Kind::Message)?;
|
let mut writer = Writer::new(file, Kind::Message)?;
|
||||||
writer.write_all(&output)?;
|
writer.write_all(&output)?;
|
||||||
|
@ -341,6 +313,11 @@ fn do_encrypt_to_self(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
options: &StringMap,
|
options: &StringMap,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let output_file = determine_valid_output_path(path, "enc", options.get("output"));
|
||||||
|
|
||||||
|
let is_armored =
|
||||||
|
options.get("armor").is_some_and(|a| a == "true") || is_extension_armored(&output_file);
|
||||||
|
|
||||||
let account = options
|
let account = options
|
||||||
.get("account")
|
.get("account")
|
||||||
.map(|account| u32::from_str(account))
|
.map(|account| u32::from_str(account))
|
||||||
|
@ -373,28 +350,23 @@ fn do_encrypt_to_self(
|
||||||
&userid.unwrap_or(UserID::from("Keyfork-Generated Key")),
|
&userid.unwrap_or(UserID::from("Keyfork-Generated Key")),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let cert_path = match options.get("output") {
|
let mut file = File::create_new(path)?;
|
||||||
Some(path) => PathBuf::from(path),
|
if is_armored {
|
||||||
None => {
|
|
||||||
let path = PathBuf::from(cert.fingerprint().to_string()).with_extension("asc");
|
|
||||||
eprintln!(
|
|
||||||
"Writing OpenPGP certificate to default path: {path}",
|
|
||||||
path = path.display()
|
|
||||||
);
|
|
||||||
path
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let file = File::create_new(&cert_path).map_err(context_stub(&cert_path))?;
|
|
||||||
let mut writer = Writer::new(file, Kind::PublicKey)?;
|
let mut writer = Writer::new(file, Kind::PublicKey)?;
|
||||||
cert.serialize(&mut writer)?;
|
cert.serialize(&mut writer)?;
|
||||||
writer.finalize()?;
|
writer.finalize()?;
|
||||||
|
} else {
|
||||||
|
cert.serialize(&mut file)?;
|
||||||
|
}
|
||||||
|
|
||||||
// a sneaky bit of DRY
|
// a sneaky bit of DRY
|
||||||
do_encrypt_to(
|
do_encrypt_to(
|
||||||
mnemonic,
|
mnemonic,
|
||||||
&cert_path,
|
path,
|
||||||
&StringMap::from([(String::from("output"), path.to_string_lossy().to_string())]),
|
&StringMap::from([(
|
||||||
|
String::from("output"),
|
||||||
|
output_file.to_string_lossy().to_string(),
|
||||||
|
)]),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -449,7 +421,7 @@ fn do_shard(
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
openpgp.shard_and_encrypt(threshold, max, mnemonic.as_bytes(), &certs[..], &mut output)?;
|
openpgp.shard_and_encrypt(threshold, max, mnemonic.as_bytes(), &certs[..], &mut output)?;
|
||||||
|
|
||||||
let mut file = File::create_new(&output_file).map_err(context_stub(&output_file))?;
|
let mut file = File::create_new(&output_file)?;
|
||||||
if is_armored {
|
if is_armored {
|
||||||
file.write_all(&output)?;
|
file.write_all(&output)?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -494,7 +466,7 @@ fn do_shard_to(
|
||||||
&mut output,
|
&mut output,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let mut file = File::create_new(&output_file).map_err(context_stub(&output_file))?;
|
let mut file = File::create_new(&output_file)?;
|
||||||
if is_armored {
|
if is_armored {
|
||||||
file.write_all(&output)?;
|
file.write_all(&output)?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -510,6 +482,10 @@ fn do_shard_to(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
#[error("missing key: {0}")]
|
||||||
|
struct MissingKey(&'static str);
|
||||||
|
|
||||||
fn do_provision(
|
fn do_provision(
|
||||||
mnemonic: &keyfork_mnemonic::Mnemonic,
|
mnemonic: &keyfork_mnemonic::Mnemonic,
|
||||||
provisioner: &provision::Provisioner,
|
provisioner: &provision::Provisioner,
|
||||||
|
@ -523,27 +499,16 @@ fn do_provision(
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let identifier = options
|
let identifier = options
|
||||||
.remove("identifier")
|
.remove("identifier")
|
||||||
.map(|s| s.split('.').map(String::from).collect::<Vec<_>>())
|
.ok_or(MissingKey("identifier"))?
|
||||||
.map(Result::<_, Box<dyn std::error::Error>>::Ok)
|
.split(',')
|
||||||
.unwrap_or_else(|| {
|
.map(String::from)
|
||||||
Ok(provisioner
|
.collect::<Vec<_>>();
|
||||||
.discover()?
|
|
||||||
.into_iter()
|
|
||||||
.map(|(identifier, _)| identifier)
|
|
||||||
.collect())
|
|
||||||
})?;
|
|
||||||
let count = options
|
let count = options
|
||||||
.remove("count")
|
.remove("count")
|
||||||
.map(|count| usize::from_str(&count))
|
.map(|count| usize::from_str(&count))
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.unwrap_or(identifier.len());
|
.unwrap_or(identifier.len());
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
count,
|
|
||||||
identifier.len(),
|
|
||||||
"amount of identifiers discovered or provided did not match provisioner count"
|
|
||||||
);
|
|
||||||
|
|
||||||
for (_, identifier) in (0..count).zip(identifier.into_iter()) {
|
for (_, identifier) in (0..count).zip(identifier.into_iter()) {
|
||||||
let provisioner_config = config::Provisioner {
|
let provisioner_config = config::Provisioner {
|
||||||
account,
|
account,
|
||||||
|
@ -593,46 +558,11 @@ impl MnemonicSubcommands {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(encrypt_to_self) = encrypt_to_self {
|
if let Some(encrypt_to_self) = encrypt_to_self {
|
||||||
let mut values = encrypt_to_self.values.clone();
|
do_encrypt_to_self(&mnemonic, &encrypt_to_self.inner, &encrypt_to_self.values)?;
|
||||||
// If we have a userid from `provision` but not one here, use that one.
|
|
||||||
if let Some(provision) = provision {
|
|
||||||
if matches!(&provision.inner, provision::Provisioner::OpenPGPCard(_))
|
|
||||||
&& !values.contains_key("userid")
|
|
||||||
{
|
|
||||||
if let Some(userid) = provision.values.get("userid") {
|
|
||||||
values.insert(String::from("userid"), userid.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
do_encrypt_to_self(&mnemonic, &encrypt_to_self.inner, &values)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(provisioner) = provision {
|
if let Some(provisioner) = provision {
|
||||||
// NOTE: If we have encrypt_to_self, we likely also have the certificate
|
do_provision(&mnemonic, &provisioner.inner, &provisioner.values)?;
|
||||||
// already generated. Therefore, we can skip generating it in the provisioner.
|
|
||||||
// However, if we don't have encrypt_to_self, we might not have the
|
|
||||||
// certificate, therefore the provisioner - by default - generates the public
|
|
||||||
// key output.
|
|
||||||
//
|
|
||||||
// We use the atypical `_skip_cert_output` field here to denote an automatic
|
|
||||||
// marking to skip the cert output. However, the `output` field will take
|
|
||||||
// priority, since it can only be manually set by the user.
|
|
||||||
let mut values = provisioner.values.clone();
|
|
||||||
if let Some(encrypt_to_self) = encrypt_to_self {
|
|
||||||
if !values.contains_key("output") {
|
|
||||||
values.insert(String::from("_skip_cert_output"), String::from("1"));
|
|
||||||
}
|
|
||||||
// If we have a userid from `encrypt_to_self` but not one here, use that
|
|
||||||
// one.
|
|
||||||
if matches!(&provisioner.inner, provision::Provisioner::OpenPGPCard(_))
|
|
||||||
&& !values.contains_key("userid")
|
|
||||||
{
|
|
||||||
if let Some(userid) = encrypt_to_self.values.get("userid") {
|
|
||||||
values.insert(String::from("userid"), userid.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
do_provision(&mnemonic, &provisioner.inner, &values)?;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(shard_to) = shard_to {
|
if let Some(shard_to) = shard_to {
|
||||||
|
|
|
@ -3,12 +3,7 @@ use crate::{config, openpgp_card::factory_reset_current_card};
|
||||||
|
|
||||||
use card_backend_pcsc::PcscBackend;
|
use card_backend_pcsc::PcscBackend;
|
||||||
use keyfork_derive_openpgp::{
|
use keyfork_derive_openpgp::{
|
||||||
openpgp::{
|
openpgp::{packet::UserID, types::KeyFlags},
|
||||||
armor::{Kind, Writer},
|
|
||||||
packet::UserID,
|
|
||||||
serialize::Serialize,
|
|
||||||
types::KeyFlags,
|
|
||||||
},
|
|
||||||
XPrv,
|
XPrv,
|
||||||
};
|
};
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::{
|
||||||
|
@ -16,7 +11,6 @@ use keyfork_prompt::{
|
||||||
validators::{SecurePinValidator, Validator},
|
validators::{SecurePinValidator, Validator},
|
||||||
};
|
};
|
||||||
use openpgp_card_sequoia::{state::Open, Card};
|
use openpgp_card_sequoia::{state::Open, Card};
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct OpenPGPCard;
|
pub struct OpenPGPCard;
|
||||||
|
@ -76,6 +70,11 @@ impl ProvisionExec for OpenPGPCard {
|
||||||
&admin_pin_validator,
|
&admin_pin_validator,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let mut has_provisioned = false;
|
||||||
|
|
||||||
|
for backend in PcscBackend::cards(None)? {
|
||||||
|
let backend = backend?;
|
||||||
|
|
||||||
let subkeys = vec![
|
let subkeys = vec![
|
||||||
KeyFlags::empty().set_certification(),
|
KeyFlags::empty().set_certification(),
|
||||||
KeyFlags::empty().set_signing(),
|
KeyFlags::empty().set_signing(),
|
||||||
|
@ -85,46 +84,14 @@ impl ProvisionExec for OpenPGPCard {
|
||||||
KeyFlags::empty().set_authentication(),
|
KeyFlags::empty().set_authentication(),
|
||||||
];
|
];
|
||||||
|
|
||||||
let userid = match provisioner.metadata.as_ref().and_then(|m| m.get("userid")) {
|
// NOTE: This User ID doesn't have meaningful context on the card.
|
||||||
Some(userid) => UserID::from(userid.as_str()),
|
// To give it a reasonable name, use `keyfork derive openpgp` or some other system that
|
||||||
None => UserID::from("Keyfork-Provisioned Key"),
|
// generates the OpenPGP certificate.
|
||||||
};
|
let userid = UserID::from("Keyfork-Provisioned Key");
|
||||||
let cert = keyfork_derive_openpgp::derive(xprv.clone(), &subkeys, &userid)?;
|
let cert = keyfork_derive_openpgp::derive(xprv.clone(), &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 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_new(cert_output)?;
|
|
||||||
let mut writer = Writer::new(cert_output_file, Kind::PublicKey)?;
|
|
||||||
cert.serialize(&mut writer)?;
|
|
||||||
writer.finalize()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut has_provisioned = false;
|
|
||||||
|
|
||||||
for backend in PcscBackend::cards(None)? {
|
|
||||||
let backend = backend?;
|
|
||||||
|
|
||||||
let result = factory_reset_current_card(
|
let result = factory_reset_current_card(
|
||||||
&mut |identifier| identifier == provisioner.identifier,
|
&mut |identifier| { identifier == provisioner.identifier },
|
||||||
user_pin.trim(),
|
user_pin.trim(),
|
||||||
admin_pin.trim(),
|
admin_pin.trim(),
|
||||||
&cert,
|
&cert,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use card_backend_pcsc::PcscBackend;
|
use card_backend_pcsc::PcscBackend;
|
||||||
use openpgp_card_sequoia::{state::Open, types::KeyType, Card, types::TouchPolicy};
|
use openpgp_card_sequoia::{state::Open, types::KeyType, Card};
|
||||||
use keyfork_derive_openpgp::openpgp::{Cert, policy::Policy};
|
use keyfork_derive_openpgp::openpgp::{Cert, policy::Policy};
|
||||||
|
|
||||||
/// Factory reset the current card so long as it does not match the last-used backend.
|
/// Factory reset the current card so long as it does not match the last-used backend.
|
||||||
|
@ -42,11 +42,8 @@ pub fn factory_reset_current_card(
|
||||||
transaction.factory_reset()?;
|
transaction.factory_reset()?;
|
||||||
let mut admin = transaction.to_admin_card("12345678")?;
|
let mut admin = transaction.to_admin_card("12345678")?;
|
||||||
admin.upload_key(signing_key, KeyType::Signing, None)?;
|
admin.upload_key(signing_key, KeyType::Signing, None)?;
|
||||||
admin.set_touch_policy(KeyType::Signing, TouchPolicy::On)?;
|
|
||||||
admin.upload_key(decryption_key, KeyType::Decryption, None)?;
|
admin.upload_key(decryption_key, KeyType::Decryption, None)?;
|
||||||
admin.set_touch_policy(KeyType::Decryption, TouchPolicy::On)?;
|
|
||||||
admin.upload_key(authentication_key, KeyType::Authentication, None)?;
|
admin.upload_key(authentication_key, KeyType::Authentication, None)?;
|
||||||
admin.set_touch_policy(KeyType::Authentication, TouchPolicy::On)?;
|
|
||||||
transaction.change_user_pin("123456", user_pin)?;
|
transaction.change_user_pin("123456", user_pin)?;
|
||||||
transaction.change_admin_pin("12345678", admin_pin)?;
|
transaction.change_admin_pin("12345678", admin_pin)?;
|
||||||
Ok(true)
|
Ok(true)
|
||||||
|
|
|
@ -15,9 +15,8 @@ decode-backend-zbar = ["dep:keyfork-zbar"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
keyfork-bug = { workspace = true }
|
keyfork-bug = { workspace = true }
|
||||||
keyfork-zbar = { workspace = true, optional = true, features = ["image"] }
|
keyfork-zbar = { workspace = true, optional = true }
|
||||||
image = { workspace = true, default-features = false, features = ["jpeg"] }
|
image = { workspace = true, default-features = false, features = ["jpeg"] }
|
||||||
rqrr = { version = "0.9.0", optional = true }
|
rqrr = { version = "0.7.0", optional = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
v4l = { workspace = true }
|
v4l = { workspace = true }
|
||||||
cfg-if = "1.0.0"
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ use std::time::Duration;
|
||||||
use keyfork_qrcode::scan_camera;
|
use keyfork_qrcode::scan_camera;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let output = scan_camera(Duration::from_secs(15), 0)?;
|
let output = scan_camera(Duration::from_secs(60 * 10), 0)?;
|
||||||
if let Some(scanned_text) = output {
|
if let Some(scanned_text) = output {
|
||||||
println!("{scanned_text}");
|
println!("{scanned_text}");
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,17 +2,18 @@
|
||||||
|
|
||||||
use keyfork_bug as bug;
|
use keyfork_bug as bug;
|
||||||
|
|
||||||
use image::{ImageBuffer, ImageReader, Luma};
|
use image::ImageReader;
|
||||||
use std::{
|
use std::{
|
||||||
io::{Cursor, Write},
|
io::{Cursor, Write},
|
||||||
process::{Command, Stdio},
|
|
||||||
time::{Duration, Instant},
|
time::{Duration, Instant},
|
||||||
|
process::{Command, Stdio},
|
||||||
};
|
};
|
||||||
use v4l::{
|
use v4l::{
|
||||||
buffer::Type,
|
buffer::Type,
|
||||||
io::{traits::CaptureStream, userptr::Stream},
|
io::{userptr::Stream, traits::CaptureStream},
|
||||||
video::Capture,
|
video::Capture,
|
||||||
Device, FourCC,
|
FourCC,
|
||||||
|
Device,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A QR code could not be generated.
|
/// A QR code could not be generated.
|
||||||
|
@ -101,73 +102,39 @@ pub fn qrencode(
|
||||||
|
|
||||||
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
|
const VIDEO_FORMAT_READ_ERROR: &str = "Failed to read video device format";
|
||||||
|
|
||||||
trait Scanner {
|
/// Continuously scan the `index`-th camera for a QR code.
|
||||||
fn scan_image(&mut self, image: ImageBuffer<Luma<u8>, Vec<u8>>) -> Option<String>;
|
///
|
||||||
}
|
/// # Errors
|
||||||
|
///
|
||||||
#[cfg(feature = "decode-backend-zbar")]
|
/// The function may return an error if the hardware is unable to scan video or if an image could
|
||||||
mod zbar {
|
/// not be decoded.
|
||||||
use super::{ImageBuffer, Luma, Scanner};
|
|
||||||
|
|
||||||
pub struct Zbar {
|
|
||||||
scanner: keyfork_zbar::image_scanner::ImageScanner,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Zbar {
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Zbar {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
scanner: keyfork_zbar::image_scanner::ImageScanner::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Scanner for Zbar {
|
|
||||||
fn scan_image(
|
|
||||||
&mut self,
|
|
||||||
image: ImageBuffer<Luma<u8>, Vec<u8>>,
|
|
||||||
) -> 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()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "decode-backend-rqrr")]
|
#[cfg(feature = "decode-backend-rqrr")]
|
||||||
mod rqrr {
|
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
||||||
use super::{ImageBuffer, Luma, Scanner};
|
let device = Device::new(index)?;
|
||||||
|
let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
|
||||||
|
fmt.fourcc = FourCC::new(b"MPG1");
|
||||||
|
device.set_format(&fmt)?;
|
||||||
|
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
pub struct Rqrr;
|
while Instant::now()
|
||||||
|
.duration_since(start)
|
||||||
impl Scanner for Rqrr {
|
< timeout
|
||||||
fn scan_image(
|
{
|
||||||
&mut self,
|
let (buffer, _) = stream.next()?;
|
||||||
image: ImageBuffer<Luma<u8>, Vec<u8>>,
|
let image = ImageReader::new(Cursor::new(buffer))
|
||||||
) -> Option<String> {
|
.with_guessed_format()?
|
||||||
|
.decode()?
|
||||||
|
.to_luma8();
|
||||||
let mut image = rqrr::PreparedImage::prepare(image);
|
let mut image = rqrr::PreparedImage::prepare(image);
|
||||||
for grid in image.detect_grids() {
|
for grid in image.detect_grids() {
|
||||||
if let Ok((_, content)) = grid.decode() {
|
if let Ok((_, content)) = grid.decode() {
|
||||||
return Some(content);
|
return Ok(Some(content))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
Ok(None)
|
||||||
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}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Continuously scan the `index`-th camera for a QR code.
|
/// Continuously scan the `index`-th camera for a QR code.
|
||||||
|
@ -176,42 +143,29 @@ 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
|
/// The function may return an error if the hardware is unable to scan video or if an image could
|
||||||
/// not be decoded.
|
/// not be decoded.
|
||||||
|
#[cfg(feature = "decode-backend-zbar")]
|
||||||
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
pub fn scan_camera(timeout: Duration, index: usize) -> Result<Option<String>, QRCodeScanError> {
|
||||||
let device = Device::new(index)?;
|
let device = Device::new(index)?;
|
||||||
let mut fmt = device
|
let mut fmt = device.format().unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
|
||||||
.format()
|
|
||||||
.unwrap_or_else(bug::panic!(VIDEO_FORMAT_READ_ERROR));
|
|
||||||
fmt.fourcc = FourCC::new(b"MPG1");
|
fmt.fourcc = FourCC::new(b"MPG1");
|
||||||
device.set_format(&fmt)?;
|
device.set_format(&fmt)?;
|
||||||
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
let mut stream = Stream::with_buffers(&device, Type::VideoCapture, 4)?;
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
cfg_if::cfg_if! {
|
let mut scanner = keyfork_zbar::image_scanner::ImageScanner::new();
|
||||||
if #[cfg(feature = "decode-backend-zbar")] {
|
|
||||||
let mut scanner = zbar::Zbar::default();
|
|
||||||
} else if #[cfg(feature = "decode-backend-rqrr")] {
|
|
||||||
let mut scanner = rqrr::Rqrr;
|
|
||||||
} else {
|
|
||||||
unimplemented!("neither decode-backend-zbar nor decode-backend-rqrr were selected")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
#[allow(unused)]
|
while Instant::now()
|
||||||
let mut count = 0;
|
.duration_since(start)
|
||||||
|
< timeout
|
||||||
while Instant::now().duration_since(start) < timeout {
|
{
|
||||||
count += 1;
|
|
||||||
let (buffer, _) = stream.next()?;
|
let (buffer, _) = stream.next()?;
|
||||||
let image = ImageReader::new(Cursor::new(buffer))
|
let image = ImageReader::new(Cursor::new(buffer))
|
||||||
.with_guessed_format()?
|
.with_guessed_format()?
|
||||||
.decode()?
|
.decode()?;
|
||||||
.to_luma8();
|
let image = keyfork_zbar::image::Image::from(image);
|
||||||
if let Some(content) = scanner.scan_image(image) {
|
for symbol in scanner.scan_image(&image) {
|
||||||
// dbg_elapsed(count, start);
|
return Ok(Some(String::from_utf8_lossy(symbol.data()).to_string()));
|
||||||
return Ok(Some(content));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// dbg_elapsed(count, start);
|
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,7 +58,7 @@ impl Image {
|
||||||
#[cfg(feature = "image")]
|
#[cfg(feature = "image")]
|
||||||
mod impls {
|
mod impls {
|
||||||
use super::*;
|
use super::*;
|
||||||
use image::{DynamicImage, GenericImageView, ImageBuffer, Luma};
|
use image::{DynamicImage, GenericImageView};
|
||||||
|
|
||||||
impl From<DynamicImage> for Image {
|
impl From<DynamicImage> for Image {
|
||||||
fn from(value: DynamicImage) -> Self {
|
fn from(value: DynamicImage) -> Self {
|
||||||
|
@ -70,17 +70,6 @@ mod impls {
|
||||||
image
|
image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ImageBuffer<Luma<u8>, Vec<u8>>> for Image {
|
|
||||||
fn from(value: ImageBuffer<Luma<u8>, Vec<u8>>) -> Self {
|
|
||||||
let mut image = Self::alloc();
|
|
||||||
let (width, height) = value.dimensions();
|
|
||||||
image.set_size(width, height);
|
|
||||||
image.set_format(b"Y800");
|
|
||||||
image.set_data(value.into_raw());
|
|
||||||
image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for Image {
|
impl Drop for Image {
|
||||||
|
|
|
@ -16,12 +16,6 @@
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! ```rust,should_panic
|
//! ```rust,should_panic
|
||||||
//! let rows = 24;
|
|
||||||
//! let input_lines_len = 25;
|
|
||||||
//! assert!(input_lines_len < rows, "{input_lines_len} can't fit in {rows} lines!");
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! ```rust,should_panic
|
|
||||||
//! use std::fs::File;
|
//! use std::fs::File;
|
||||||
//! use keyfork_bug as bug;
|
//! use keyfork_bug as bug;
|
||||||
//!
|
//!
|
||||||
|
@ -89,29 +83,6 @@ macro_rules! bug {
|
||||||
}};
|
}};
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Assert a condition is true, otherwise throwing an error using Keyfork Bug.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```rust
|
|
||||||
/// let expectations = "conceivable!";
|
|
||||||
/// let circumstances = "otherwise";
|
|
||||||
/// assert!(circumstances != expectations, "you keep using that word...");
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// Variables can be used in the error message, without having to pass them manually.
|
|
||||||
///
|
|
||||||
/// ```rust,should_panic
|
|
||||||
/// let rows = 24;
|
|
||||||
/// let input_lines_len = 25;
|
|
||||||
/// assert!(input_lines_len < rows, "{input_lines_len} can't fit in {rows} lines!");
|
|
||||||
/// ```
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! assert {
|
|
||||||
($cond:expr, $($input:tt)*) => {
|
|
||||||
std::assert!($cond, "{}", keyfork_bug::bug!($($input)*));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Return a closure that, when called, panics with a bug report message for Keyfork. Returning a
|
/// Return a closure that, when called, panics with a bug report message for Keyfork. Returning a
|
||||||
/// closure can help handle the `clippy::expect_fun_call` lint. The closure accepts an error
|
/// closure can help handle the `clippy::expect_fun_call` lint. The closure accepts an error
|
||||||
/// argument, so it is suitable for being used with [`Result`] types instead of [`Option`] types.
|
/// argument, so it is suitable for being used with [`Result`] types instead of [`Option`] types.
|
||||||
|
|
|
@ -1,47 +1,15 @@
|
||||||
#![allow(missing_docs)]
|
#![allow(missing_docs)]
|
||||||
|
|
||||||
use keyfork_prompt::default_handler;
|
use keyfork_prompt::{
|
||||||
|
Message,
|
||||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
default_handler,
|
||||||
pub enum Choices {
|
};
|
||||||
Retry,
|
|
||||||
Continue,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Choices {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
Choices::Retry => write!(
|
|
||||||
f,
|
|
||||||
"Retry with some really long text that I want to cause issues with."
|
|
||||||
),
|
|
||||||
Choices::Continue => write!(
|
|
||||||
f,
|
|
||||||
"Continue with some really long text that I want to cause issues with."
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl keyfork_prompt::Choice for Choices {
|
|
||||||
fn identifier(&self) -> Option<char> {
|
|
||||||
Some(match self {
|
|
||||||
Choices::Retry => 'r',
|
|
||||||
Choices::Continue => 'c',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut handler = default_handler()?;
|
let mut handler = default_handler()?;
|
||||||
|
|
||||||
let choice = keyfork_prompt::prompt_choice(
|
let output = handler.prompt_input("Test message: ")?;
|
||||||
&mut *handler,
|
handler.prompt_message(Message::Text(format!("Result: {output}")))?;
|
||||||
"Here are some options!",
|
|
||||||
&[Choices::Retry, Choices::Continue],
|
|
||||||
);
|
|
||||||
|
|
||||||
dbg!(&choice);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,12 +4,9 @@
|
||||||
//! directly intended to be machine-readable, but can be used for scriptable automation in a
|
//! directly intended to be machine-readable, but can be used for scriptable automation in a
|
||||||
//! fashion similar to a terminal handler.
|
//! fashion similar to a terminal handler.
|
||||||
|
|
||||||
use std::{
|
use std::io::{IsTerminal, Write};
|
||||||
io::{IsTerminal, Write},
|
|
||||||
str::FromStr,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{BoxResult, Choice, Error, Message, PromptHandler, Result};
|
use crate::{BoxResult, Error, Message, PromptHandler, Result};
|
||||||
|
|
||||||
/// A headless prompt handler, usable in situations when a terminal might not be available, or for
|
/// A headless prompt handler, usable in situations when a terminal might not be available, or for
|
||||||
/// scripting purposes where manual input from a terminal is not desirable.
|
/// scripting purposes where manual input from a terminal is not desirable.
|
||||||
|
@ -61,47 +58,17 @@ impl PromptHandler for Headless {
|
||||||
fn prompt_message(&mut self, prompt: Message) -> Result<()> {
|
fn prompt_message(&mut self, prompt: Message) -> Result<()> {
|
||||||
match prompt {
|
match prompt {
|
||||||
Message::Text(s) => {
|
Message::Text(s) => {
|
||||||
writeln!(&mut self.stderr, "{s}")?;
|
self.stderr.write_all(s.as_bytes())?;
|
||||||
self.stderr.flush()?;
|
self.stderr.flush()?;
|
||||||
}
|
}
|
||||||
Message::Data(s) => {
|
Message::Data(s) => {
|
||||||
writeln!(&mut self.stderr, "{s}")?;
|
self.stderr.write_all(s.as_bytes())?;
|
||||||
self.stderr.flush()?;
|
self.stderr.flush()?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
writeln!(&mut self.stderr, "Press enter to continue.")?;
|
|
||||||
self.stdin.read_line(&mut String::new())?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_choice_num(&mut self, prompt: &str, choices: &[Box<dyn Choice>]) -> Result<usize> {
|
|
||||||
writeln!(&mut self.stderr, "{prompt}")?;
|
|
||||||
for (i, choice) in choices.iter().enumerate() {
|
|
||||||
match choice.identifier() {
|
|
||||||
Some(identifier) => {
|
|
||||||
writeln!(&mut self.stderr, "{i}. ({identifier})\t{choice}")?;
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
writeln!(&mut self.stderr, "{i}.\t{choice}")?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.stderr.flush()?;
|
|
||||||
let mut line = String::new();
|
|
||||||
self.stdin.read_line(&mut line)?;
|
|
||||||
let selector_char = line.chars().next();
|
|
||||||
if let Some(selector @ ('a'..='z' | 'A'..='Z')) = selector_char {
|
|
||||||
if let Some((index, _)) = choices.iter().enumerate().find(|(_, choice)| {
|
|
||||||
choice
|
|
||||||
.identifier()
|
|
||||||
.is_some_and(|identifier| selector == identifier)
|
|
||||||
}) {
|
|
||||||
return Ok(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
usize::from_str(line.trim()).map_err(|e| Error::Custom(e.to_string()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prompt_validated_wordlist(
|
fn prompt_validated_wordlist(
|
||||||
&mut self,
|
&mut self,
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
|
@ -118,7 +85,7 @@ impl PromptHandler for Headless {
|
||||||
self.stdin.read_line(&mut line)?;
|
self.stdin.read_line(&mut line)?;
|
||||||
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
|
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
|
||||||
last_error = e.to_string();
|
last_error = e.to_string();
|
||||||
writeln!(&mut self.stderr, "{e}")?;
|
self.stderr.write_all(e.to_string().as_bytes())?;
|
||||||
self.stderr.flush()?;
|
self.stderr.flush()?;
|
||||||
} else {
|
} else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
@ -141,7 +108,8 @@ impl PromptHandler for Headless {
|
||||||
self.stdin.read_line(&mut line)?;
|
self.stdin.read_line(&mut line)?;
|
||||||
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
|
if let Err(e) = validator_fn(std::mem::take(&mut line)) {
|
||||||
last_error = e.to_string();
|
last_error = e.to_string();
|
||||||
writeln!(&mut self.stderr, "{e}")?;
|
self.stderr.write_all(e.to_string().as_bytes())?;
|
||||||
|
self.stderr.write_all(b"\n")?;
|
||||||
self.stderr.flush()?;
|
self.stderr.flush()?;
|
||||||
} else {
|
} else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|
|
@ -50,10 +50,6 @@ pub enum Error {
|
||||||
/// An error occurred while interacting with a terminal.
|
/// An error occurred while interacting with a terminal.
|
||||||
#[error("IO Error: {0}")]
|
#[error("IO Error: {0}")]
|
||||||
IO(#[from] std::io::Error),
|
IO(#[from] std::io::Error),
|
||||||
|
|
||||||
/// An unexpected error occurred.
|
|
||||||
#[error("{0}")]
|
|
||||||
Custom(String),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
|
@ -68,21 +64,6 @@ pub enum Message {
|
||||||
Data(String),
|
Data(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A type that may represent an identifier to be used when using a choice prompt.
|
|
||||||
pub trait Choice: std::fmt::Display {
|
|
||||||
/// The identifier for the type.
|
|
||||||
fn identifier(&self) -> Option<char> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// this way, we can make Box<dyn T> from &T
|
|
||||||
impl<T: Choice> Choice for &T {
|
|
||||||
fn identifier(&self) -> Option<char> {
|
|
||||||
Choice::identifier(*self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub type BoxResult = std::result::Result<(), Box<dyn std::error::Error>>;
|
pub type BoxResult = std::result::Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
@ -117,16 +98,6 @@ pub trait PromptHandler {
|
||||||
/// occurred while waiting for the user to dismiss the message.
|
/// occurred while waiting for the user to dismiss the message.
|
||||||
fn prompt_message(&mut self, prompt: Message) -> Result<()>;
|
fn prompt_message(&mut self, prompt: Message) -> Result<()>;
|
||||||
|
|
||||||
/// Prompt the user for a choice between the provided options. The returned value is the index
|
|
||||||
/// of the given choice.
|
|
||||||
///
|
|
||||||
/// This method SHOULD NOT be used directly. Instead, use [`prompt_choice`].
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// The method may return an error if the message was not able to be displayed or if the input
|
|
||||||
/// could not be read.
|
|
||||||
fn prompt_choice_num(&mut self, prompt: &str, choices: &[Box<dyn Choice>]) -> Result<usize>;
|
|
||||||
|
|
||||||
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
|
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
|
||||||
/// provided parser function, returning the type from the parser. A language must be specified
|
/// provided parser function, returning the type from the parser. A language must be specified
|
||||||
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
|
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
|
||||||
|
@ -162,29 +133,6 @@ pub trait PromptHandler {
|
||||||
) -> Result<(), Error>;
|
) -> Result<(), Error>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prompt the user for a choice between the provided options. The returned value is the selected
|
|
||||||
/// choice.
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
/// The method may return an error if the message was not able to be displayed or if the input
|
|
||||||
/// could not be read.
|
|
||||||
#[allow(clippy::missing_panics_doc)]
|
|
||||||
pub fn prompt_choice<T>(
|
|
||||||
handler: &mut dyn PromptHandler,
|
|
||||||
prompt: &str,
|
|
||||||
choices: &'static [T],
|
|
||||||
) -> Result<T>
|
|
||||||
where
|
|
||||||
T: Choice + Copy + 'static,
|
|
||||||
{
|
|
||||||
let boxed_choices = choices
|
|
||||||
.iter()
|
|
||||||
.map(|c| Box::new(c) as Box<dyn Choice>)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let choice = handler.prompt_choice_num(prompt, boxed_choices.as_slice())?;
|
|
||||||
Ok(choices[choice])
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
|
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
|
||||||
/// provided parser function, returning the type from the parser. A language must be specified
|
/// provided parser function, returning the type from the parser. A language must be specified
|
||||||
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
|
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
|
||||||
|
|
|
@ -21,7 +21,7 @@ use keyfork_crossterm::{
|
||||||
|
|
||||||
use keyfork_bug::bug;
|
use keyfork_bug::bug;
|
||||||
|
|
||||||
use crate::{BoxResult, Choice, Error, Message, PromptHandler};
|
use crate::{BoxResult, Error, Message, PromptHandler};
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
@ -129,26 +129,14 @@ where
|
||||||
{
|
{
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.write
|
self.write
|
||||||
.execute(cursor::EnableBlinking)
|
|
||||||
.expect(bug!("can't enable blinking"))
|
|
||||||
.execute(cursor::Show)
|
|
||||||
.expect(bug!("can't show cursor"))
|
|
||||||
.execute(DisableBracketedPaste)
|
.execute(DisableBracketedPaste)
|
||||||
.expect(bug!("can't restore bracketed paste"));
|
.expect(bug!("can't restore bracketed paste"));
|
||||||
|
self.write
|
||||||
|
.execute(LeaveAlternateScreen)
|
||||||
|
.expect(bug!("can't leave alternate screen"));
|
||||||
self.terminal
|
self.terminal
|
||||||
.disable_raw_mode()
|
.disable_raw_mode()
|
||||||
.expect(bug!("can't disable raw mode"));
|
.expect(bug!("can't disable raw mode"));
|
||||||
// we don't want to clear error messages
|
|
||||||
if !std::thread::panicking() {
|
|
||||||
self.write
|
|
||||||
.queue(LeaveAlternateScreen)
|
|
||||||
.expect(bug!("can't leave alternate screen"))
|
|
||||||
.queue(terminal::Clear(terminal::ClearType::All))
|
|
||||||
.expect(bug!("can't clear screen"))
|
|
||||||
.queue(cursor::MoveTo(0, 0))
|
|
||||||
.expect(bug!("can't move to origin"));
|
|
||||||
}
|
|
||||||
self.write.flush().expect(bug!("can't execute terminal reset commands"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,7 +188,9 @@ where
|
||||||
prefix_length = line.len();
|
prefix_length = line.len();
|
||||||
terminal.queue(Print(line))?;
|
terminal.queue(Print(line))?;
|
||||||
if lines.peek().is_some() {
|
if lines.peek().is_some() {
|
||||||
terminal.queue(cursor::MoveToNextLine(1))?;
|
terminal
|
||||||
|
.queue(cursor::MoveDown(1))?
|
||||||
|
.queue(cursor::MoveToColumn(0))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
terminal.flush()?;
|
terminal.flush()?;
|
||||||
|
@ -267,103 +257,6 @@ where
|
||||||
Ok(input)
|
Ok(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_choice_num(&mut self, prompt: &str, choices: &[Box<dyn Choice>]) -> Result<usize> {
|
|
||||||
let mut terminal = self.lock().alternate_screen()?.raw_mode()?;
|
|
||||||
terminal
|
|
||||||
.queue(terminal::Clear(terminal::ClearType::All))?
|
|
||||||
.queue(cursor::MoveTo(0, 0))?
|
|
||||||
.queue(cursor::Hide)?;
|
|
||||||
|
|
||||||
for line in prompt.lines() {
|
|
||||||
terminal
|
|
||||||
.queue(Print(line))?
|
|
||||||
.queue(cursor::MoveToNextLine(1))?;
|
|
||||||
|
|
||||||
terminal.flush()?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut active_choice = 0;
|
|
||||||
let mut drawn = false;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let (cols, rows) = terminal.size()?;
|
|
||||||
// all choices, plus their padding, plus the spacing between, minus whitespace at end.
|
|
||||||
let max_size = choices
|
|
||||||
.iter()
|
|
||||||
.fold(0usize, |agg, choice| agg + choice.to_string().len() + 2)
|
|
||||||
+ std::cmp::max(choices.len(), 1)
|
|
||||||
- 1;
|
|
||||||
let horizontal = max_size < cols.into();
|
|
||||||
keyfork_bug::assert!(
|
|
||||||
horizontal || usize::from(rows) > prompt.lines().count() + choices.len(),
|
|
||||||
"screen too small, can't fit choices on {rows}x{cols}",
|
|
||||||
);
|
|
||||||
if horizontal {
|
|
||||||
terminal.queue(cursor::MoveToColumn(0))?;
|
|
||||||
} else if drawn {
|
|
||||||
terminal
|
|
||||||
.queue(cursor::MoveUp(
|
|
||||||
choices
|
|
||||||
.len()
|
|
||||||
.saturating_sub(1)
|
|
||||||
.try_into()
|
|
||||||
.expect(keyfork_bug::bug!("more than {} choices provided", u16::MAX)),
|
|
||||||
))?
|
|
||||||
.queue(cursor::MoveToColumn(0))?;
|
|
||||||
} else {
|
|
||||||
drawn = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut iter = choices.iter().enumerate().peekable();
|
|
||||||
while let Some((i, choice)) = iter.next() {
|
|
||||||
// if active choice, flip foreground and background
|
|
||||||
// if active choice, wrap in []
|
|
||||||
// if not, wrap in spaces, to preserve spacing and prevent redraws
|
|
||||||
if i == active_choice {
|
|
||||||
terminal.queue(PrintStyledContent(Stylize::reverse(format!("[{choice}]"))))?;
|
|
||||||
} else {
|
|
||||||
terminal.queue(Print(format!(" {choice} ")))?;
|
|
||||||
}
|
|
||||||
if iter.peek().is_some() {
|
|
||||||
if horizontal {
|
|
||||||
terminal.queue(Print(" "))?;
|
|
||||||
} else {
|
|
||||||
terminal.queue(cursor::MoveToNextLine(1))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
terminal.flush()?;
|
|
||||||
|
|
||||||
if let Event::Key(k) = read()? {
|
|
||||||
match k.code {
|
|
||||||
KeyCode::Char('c') if k.modifiers.contains(KeyModifiers::CONTROL) => {
|
|
||||||
return Err(Error::CtrlC);
|
|
||||||
}
|
|
||||||
KeyCode::Char(c) => {
|
|
||||||
for (i, choice) in choices.iter().enumerate() {
|
|
||||||
if choice.identifier().is_some_and(|id| id == c) {
|
|
||||||
active_choice = i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
KeyCode::Left | KeyCode::Up => {
|
|
||||||
active_choice = active_choice.saturating_sub(1);
|
|
||||||
}
|
|
||||||
KeyCode::Right | KeyCode::Down => match choices.len().saturating_sub(active_choice) {
|
|
||||||
0 | 1 => {}
|
|
||||||
_ => {
|
|
||||||
active_choice += 1;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
KeyCode::Enter => {
|
|
||||||
return Ok(active_choice);
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prompt_validated_wordlist(
|
fn prompt_validated_wordlist(
|
||||||
&mut self,
|
&mut self,
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
|
@ -407,7 +300,9 @@ where
|
||||||
prefix_length = line.len();
|
prefix_length = line.len();
|
||||||
terminal.queue(Print(line))?;
|
terminal.queue(Print(line))?;
|
||||||
if lines.peek().is_some() {
|
if lines.peek().is_some() {
|
||||||
terminal.queue(cursor::MoveToNextLine(1))?;
|
terminal
|
||||||
|
.queue(cursor::MoveDown(1))?
|
||||||
|
.queue(cursor::MoveToColumn(0))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
terminal.flush()?;
|
terminal.flush()?;
|
||||||
|
@ -566,7 +461,9 @@ where
|
||||||
prefix_length = line.len();
|
prefix_length = line.len();
|
||||||
terminal.queue(Print(line))?;
|
terminal.queue(Print(line))?;
|
||||||
if lines.peek().is_some() {
|
if lines.peek().is_some() {
|
||||||
terminal.queue(cursor::MoveToNextLine(1))?;
|
terminal
|
||||||
|
.queue(cursor::MoveDown(1))?
|
||||||
|
.queue(cursor::MoveToColumn(0))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
terminal.flush()?;
|
terminal.flush()?;
|
||||||
|
@ -632,17 +529,21 @@ where
|
||||||
let len = std::cmp::min(u16::MAX as usize, word.len()) as u16;
|
let len = std::cmp::min(u16::MAX as usize, word.len()) as u16;
|
||||||
written_chars += len + 1;
|
written_chars += len + 1;
|
||||||
if written_chars > cols {
|
if written_chars > cols {
|
||||||
terminal.queue(cursor::MoveToNextLine(1))?;
|
terminal
|
||||||
|
.queue(cursor::MoveDown(1))?
|
||||||
|
.queue(cursor::MoveToColumn(0))?;
|
||||||
written_chars = len + 1;
|
written_chars = len + 1;
|
||||||
}
|
}
|
||||||
terminal.queue(Print(word))?.queue(Print(" "))?;
|
terminal.queue(Print(word))?.queue(Print(" "))?;
|
||||||
}
|
}
|
||||||
terminal.queue(cursor::MoveToNextLine(1))?;
|
terminal
|
||||||
|
.queue(cursor::MoveDown(1))?
|
||||||
|
.queue(cursor::MoveToColumn(0))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Data(data) => {
|
Message::Data(data) => {
|
||||||
let count = data.lines().count();
|
let count = data.lines().count();
|
||||||
// NOTE: GE to allow a MoveToNextLine(1)
|
// NOTE: GE to allow a MoveDown(1)
|
||||||
if count >= rows as usize {
|
if count >= rows as usize {
|
||||||
let msg = format!(
|
let msg = format!(
|
||||||
"{} {count} {} {rows} {}",
|
"{} {count} {} {rows} {}",
|
||||||
|
@ -650,12 +551,14 @@ where
|
||||||
);
|
);
|
||||||
terminal
|
terminal
|
||||||
.queue(Print(msg))?
|
.queue(Print(msg))?
|
||||||
.queue(cursor::MoveToNextLine(1))?;
|
.queue(cursor::MoveDown(1))?
|
||||||
|
.queue(cursor::MoveToColumn(0))?;
|
||||||
} else {
|
} else {
|
||||||
for line in data.lines() {
|
for line in data.lines() {
|
||||||
terminal
|
terminal
|
||||||
.queue(Print(line))?
|
.queue(Print(line))?
|
||||||
.queue(cursor::MoveToNextLine(1))?;
|
.queue(cursor::MoveDown(1))?
|
||||||
|
.queue(cursor::MoveToColumn(0))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -677,6 +580,7 @@ where
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
terminal.queue(cursor::EnableBlinking)?.flush()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue