Skip to main content

auths_core/pairing/
qr.rs

1//! QR code generation for pairing tokens.
2
3use qrcode::QrCode;
4use qrcode::render::unicode;
5
6use super::error::PairingError;
7use super::token::PairingToken;
8
9/// QR code rendering options.
10#[derive(Debug, Clone)]
11pub struct QrOptions {
12    /// Use compact rendering (half-block characters).
13    pub compact: bool,
14    /// Quiet zone (border) size in modules.
15    pub quiet_zone: u32,
16    /// Invert colors (light on dark).
17    pub invert: bool,
18}
19
20impl Default for QrOptions {
21    fn default() -> Self {
22        Self {
23            compact: true,
24            quiet_zone: 1,
25            invert: false,
26        }
27    }
28}
29
30/// Render a pairing token as a QR code string for terminal display.
31pub fn render_qr(token: &PairingToken, options: &QrOptions) -> Result<String, PairingError> {
32    let uri = token.to_uri();
33    render_qr_from_data(&uri, options)
34}
35
36/// Render arbitrary data as a QR code string for terminal display.
37pub fn render_qr_from_data(data: &str, options: &QrOptions) -> Result<String, PairingError> {
38    let code =
39        QrCode::new(data.as_bytes()).map_err(|e| PairingError::QrCodeFailed(e.to_string()))?;
40
41    let qr_string = if options.compact {
42        // Use half-block characters for compact rendering
43        let (dark, light) = if options.invert {
44            (unicode::Dense1x2::Light, unicode::Dense1x2::Dark)
45        } else {
46            (unicode::Dense1x2::Dark, unicode::Dense1x2::Light)
47        };
48
49        code.render::<unicode::Dense1x2>()
50            .dark_color(dark)
51            .light_color(light)
52            .quiet_zone(options.quiet_zone > 0)
53            .build()
54    } else {
55        // Use full-block characters
56        let (dark, light) = if options.invert {
57            (' ', '\u{2588}')
58        } else {
59            ('\u{2588}', ' ')
60        };
61
62        code.render::<char>()
63            .dark_color(dark)
64            .light_color(light)
65            .quiet_zone(options.quiet_zone > 0)
66            .build()
67    };
68
69    Ok(qr_string)
70}
71
72/// Format a pairing QR code with header text for terminal display.
73///
74/// Returns the complete formatted output as a String. The caller is
75/// responsible for printing it.
76pub fn format_pairing_qr(token: &PairingToken) -> Result<String, PairingError> {
77    let options = QrOptions::default();
78    let qr = render_qr(token, &options)?;
79
80    let mut output = String::new();
81    output.push('\n');
82    output.push_str("Scan this QR code with your other device:\n");
83    output.push('\n');
84    output.push_str(&qr);
85    output.push('\n');
86    output.push('\n');
87    output.push_str(&format!(
88        "Or enter this code manually: {}\n",
89        token.short_code
90    ));
91    output.push('\n');
92    output.push_str(&format!("Controller: {}\n", token.controller_did));
93    if !token.capabilities.is_empty() {
94        output.push_str(&format!(
95            "Capabilities: {}\n",
96            token.capabilities.join(", ")
97        ));
98    }
99    output.push_str(&format!(
100        "Expires: {}\n",
101        token.expires_at.format("%H:%M:%S")
102    ));
103    output.push('\n');
104
105    Ok(output)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    fn make_token() -> PairingToken {
113        PairingToken::generate(
114            chrono::Utc::now(),
115            "did:keri:test123".to_string(),
116            "http://localhost:3000".to_string(),
117            vec!["sign_commit".to_string()],
118        )
119        .unwrap()
120        .token
121    }
122
123    #[test]
124    fn test_render_qr() {
125        let token = make_token();
126        let options = QrOptions::default();
127
128        let qr = render_qr(&token, &options).unwrap();
129        assert!(!qr.is_empty());
130        // Should contain unicode block characters
131        assert!(qr.contains('\u{2580}') || qr.contains('\u{2584}') || qr.contains('\u{2588}'));
132    }
133
134    #[test]
135    fn test_render_qr_inverted() {
136        let token = make_token();
137        let options = QrOptions {
138            invert: true,
139            ..Default::default()
140        };
141
142        let qr = render_qr(&token, &options).unwrap();
143        assert!(!qr.is_empty());
144    }
145
146    #[test]
147    fn test_render_qr_non_compact() {
148        let token = make_token();
149        let options = QrOptions {
150            compact: false,
151            ..Default::default()
152        };
153
154        let qr = render_qr(&token, &options).unwrap();
155        assert!(!qr.is_empty());
156    }
157}