Skip to main content

ftui_core/
lib.rs

1// Forbid unsafe in production; deny (with targeted allows) in tests for env var helpers.
2#![cfg_attr(not(test), forbid(unsafe_code))]
3#![cfg_attr(test, deny(unsafe_code))]
4
5//! Core: terminal lifecycle, capability detection, events, and input parsing.
6//!
7//! # Role in FrankenTUI
8//! `ftui-core` is the input layer. It owns terminal session setup/teardown,
9//! capability probing, and normalized event types that the runtime consumes.
10//!
11//! # Primary responsibilities
12//! - **TerminalSession**: RAII lifecycle for raw mode, alt-screen, and cleanup.
13//! - **Event**: canonical input events (keys, mouse, paste, resize, focus).
14//! - **Capability detection**: terminal features and overrides.
15//! - **Input parsing**: robust decoding of terminal input streams.
16//!
17//! # How it fits in the system
18//! The runtime (`ftui-runtime`) consumes `ftui-core::Event` values and drives
19//! application models. The render kernel (`ftui-render`) is independent of
20//! input, so `ftui-core` is the clean bridge between terminal I/O and the
21//! deterministic render pipeline.
22
23pub mod animation;
24pub mod capability_override;
25pub mod cursor;
26pub mod cx;
27pub mod event;
28pub mod event_coalescer;
29pub mod generic_diff;
30pub mod generic_repr;
31pub mod geometry;
32pub mod gesture;
33pub mod glyph_policy;
34pub mod hover_stabilizer;
35pub mod inline_mode;
36pub mod input_parser;
37pub mod key_sequence;
38pub mod keybinding;
39pub mod logging;
40pub mod mode_typestate;
41pub mod mux_passthrough;
42pub mod read_optimized;
43pub mod s3_fifo;
44pub mod semantic_event;
45pub mod terminal_capabilities;
46#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
47pub mod terminal_session;
48#[cfg(all(not(target_arch = "wasm32"), feature = "crossterm"))]
49pub use terminal_session::with_panic_cleanup_suppressed;
50#[cfg(not(all(not(target_arch = "wasm32"), feature = "crossterm")))]
51#[inline]
52pub fn with_panic_cleanup_suppressed<F, R>(f: F) -> R
53where
54    F: FnOnce() -> R,
55{
56    f()
57}
58
59/// Feature-off mirror of [`terminal_session`] for builds without the
60/// crossterm backend. Nothing else owns the terminal writer in that
61/// configuration, so the one-writer output lock degrades to a no-op guard;
62/// downstream crates (e.g. franken_node's operator surface) can keep calling
63/// [`terminal_output_lock`] unconditionally.
64#[cfg(not(all(not(target_arch = "wasm32"), feature = "crossterm")))]
65pub mod terminal_session {
66    /// Guard returned by the no-op [`terminal_output_lock`] stub.
67    #[derive(Debug, Default, Clone, Copy)]
68    pub struct TerminalOutputGuard;
69
70    /// Serialize terminal writes. Without crossterm there is no raw-mode
71    /// writer to contend with, so this is a no-op.
72    #[inline]
73    #[must_use]
74    pub fn terminal_output_lock() -> TerminalOutputGuard {
75        TerminalOutputGuard
76    }
77}
78
79pub mod shutdown_signal {
80    //! Process-wide graceful-termination signal state shared by runtime and backends.
81    //!
82    //! Signal handlers record the first pending termination signal here. The
83    //! runtime polls it, performs graceful teardown, then clears it to
84    //! acknowledge completion back to the signal thread.
85
86    use std::sync::{
87        Mutex, OnceLock,
88        atomic::{AtomicI32, Ordering},
89    };
90
91    static PENDING_TERMINATION_SIGNAL: AtomicI32 = AtomicI32::new(0);
92
93    /// Record that a termination signal was intercepted and graceful shutdown is required.
94    ///
95    /// The first pending signal wins until the runtime explicitly clears it
96    /// after finishing teardown.
97    pub fn record_pending_termination_signal(signal: i32) {
98        let _ = PENDING_TERMINATION_SIGNAL.compare_exchange(
99            0,
100            signal,
101            Ordering::SeqCst,
102            Ordering::SeqCst,
103        );
104    }
105
106    /// Inspect the currently pending termination signal, if any.
107    #[must_use]
108    pub fn pending_termination_signal() -> Option<i32> {
109        match PENDING_TERMINATION_SIGNAL.load(Ordering::SeqCst) {
110            0 => None,
111            signal => Some(signal),
112        }
113    }
114
115    /// Clear any pending graceful-termination request.
116    pub fn clear_pending_termination_signal() {
117        PENDING_TERMINATION_SIGNAL.store(0, Ordering::SeqCst);
118    }
119
120    /// Serialize tests that touch the process-global termination signal slot.
121    ///
122    /// This helper is intentionally exported so downstream workspace crates can
123    /// wrap signal-sensitive tests with the same lock. Without cross-crate
124    /// serialization, parallel test execution can clear the pending signal out
125    /// from under a runtime test and leave it blocked in the event loop.
126    #[doc(hidden)]
127    pub fn with_test_signal_serialization<R>(f: impl FnOnce() -> R) -> R {
128        static SIGNAL_TEST_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
129
130        let _guard = SIGNAL_TEST_LOCK
131            .get_or_init(|| Mutex::new(()))
132            .lock()
133            .expect("shutdown signal test lock poisoned");
134        clear_pending_termination_signal();
135        let result = f();
136        clear_pending_termination_signal();
137        result
138    }
139}
140
141#[cfg(feature = "caps-probe")]
142pub mod caps_probe;
143
144// Re-export tracing macros at crate root for ergonomic use.
145#[cfg(feature = "tracing")]
146pub use logging::{
147    debug, debug_span, error, error_span, info, info_span, trace, trace_span, warn, warn_span,
148};
149
150pub mod text_width {
151    //! Shared display width helpers for layout and rendering.
152    //!
153    //! This module centralizes glyph width calculation so layout (ftui-text)
154    //! and rendering (ftui-render) stay in lockstep. It intentionally avoids
155    //! ad-hoc emoji heuristics and relies on Unicode data tables.
156    //!
157    //! ## Emoji Width Handling
158    //!
159    //! Most terminals render **text-default** emoji (those with
160    //! `Emoji_Presentation=No`, like U+2764 RED HEART) at **width 1**, even
161    //! when a Variation Selector 16 (U+FE0F) is appended. The Unicode spec
162    //! says VS16 requests emoji presentation (width 2), but terminal reality
163    //! disagrees.
164    //!
165    //! **Default behavior** (`FTUI_EMOJI_VS16_WIDTH` unset):
166    //! - `strip_vs16` removes U+FE0F before width calculation.
167    //! - Text-default emoji render at width 1 (matching most terminals).
168    //! - Emoji with `Emoji_Presentation=Yes` (e.g. U+1F600) are unaffected
169    //!   — they are always width 2.
170    //!
171    //! **Opt-in** for terminals that correctly render VS16 at width 2
172    //! (WezTerm, Kitty, Ghostty):
173    //! ```text
174    //! FTUI_EMOJI_VS16_WIDTH=unicode   # or =2
175    //! ```
176    //!
177    //! The policy is read once at startup via [`OnceLock`]. Changing the env
178    //! var mid-process has no effect. See [`vs16_width_trusted`] and
179    //! [`vs16_trust_from_env`] for the API surface.
180
181    use std::sync::OnceLock;
182
183    use unicode_display_width::width as unicode_display_width;
184    use unicode_segmentation::UnicodeSegmentation;
185    use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
186
187    #[inline]
188    fn env_flag(value: &str) -> bool {
189        matches!(
190            value.trim().to_ascii_lowercase().as_str(),
191            "1" | "true" | "yes" | "on"
192        )
193    }
194
195    #[inline]
196    fn is_cjk_locale(locale: &str) -> bool {
197        let lower = locale.trim().to_ascii_lowercase();
198        lower.starts_with("ja") || lower.starts_with("zh") || lower.starts_with("ko")
199    }
200
201    #[inline]
202    fn cjk_width_from_env_impl<F>(get_env: F) -> bool
203    where
204        F: Fn(&str) -> Option<String>,
205    {
206        if let Some(value) = get_env("FTUI_GLYPH_DOUBLE_WIDTH") {
207            return env_flag(&value);
208        }
209        if let Some(value) = get_env("FTUI_TEXT_CJK_WIDTH").or_else(|| get_env("FTUI_CJK_WIDTH")) {
210            return env_flag(&value);
211        }
212        if let Some(locale) = get_env("LC_CTYPE").or_else(|| get_env("LANG")) {
213            return is_cjk_locale(&locale);
214        }
215        false
216    }
217
218    #[inline]
219    fn use_cjk_width() -> bool {
220        static CJK_WIDTH: OnceLock<bool> = OnceLock::new();
221        *CJK_WIDTH.get_or_init(|| cjk_width_from_env_impl(|key| std::env::var(key).ok()))
222    }
223
224    /// Whether the terminal is trusted to render text-default emoji + VS16 at
225    /// width 2 (matching the Unicode spec).  Most terminals do NOT — they
226    /// render these at width 1 — so the default is `false`.
227    ///
228    /// Set `FTUI_EMOJI_VS16_WIDTH=unicode` (or `=2`) to opt in for terminals
229    /// that handle this correctly (WezTerm, Kitty, Ghostty).
230    #[inline]
231    fn trust_vs16_width() -> bool {
232        static TRUST: OnceLock<bool> = OnceLock::new();
233        *TRUST.get_or_init(|| {
234            std::env::var("FTUI_EMOJI_VS16_WIDTH")
235                .map(|v| v.eq_ignore_ascii_case("unicode") || v == "2")
236                .unwrap_or(false)
237        })
238    }
239
240    /// Compute VS16 trust policy using a custom environment lookup (testable).
241    #[inline]
242    pub fn vs16_trust_from_env<F>(get_env: F) -> bool
243    where
244        F: Fn(&str) -> Option<String>,
245    {
246        get_env("FTUI_EMOJI_VS16_WIDTH")
247            .map(|v| v.eq_ignore_ascii_case("unicode") || v == "2")
248            .unwrap_or(false)
249    }
250
251    /// Cached VS16 width trust policy (fast path).
252    #[inline]
253    pub fn vs16_width_trusted() -> bool {
254        trust_vs16_width()
255    }
256
257    /// Strip U+FE0F (VS16) from a grapheme cluster.  Returns `None` if the
258    /// grapheme does not contain VS16 (no allocation needed).
259    #[inline]
260    fn strip_vs16(grapheme: &str) -> Option<String> {
261        if grapheme.contains('\u{FE0F}') {
262            Some(grapheme.chars().filter(|&c| c != '\u{FE0F}').collect())
263        } else {
264            None
265        }
266    }
267
268    /// Compute CJK width policy using a custom environment lookup.
269    #[inline]
270    pub fn cjk_width_from_env<F>(get_env: F) -> bool
271    where
272        F: Fn(&str) -> Option<String>,
273    {
274        cjk_width_from_env_impl(get_env)
275    }
276
277    /// Cached CJK width policy (fast path).
278    #[inline]
279    pub fn cjk_width_enabled() -> bool {
280        use_cjk_width()
281    }
282
283    #[inline]
284    fn ascii_display_width(text: &str) -> usize {
285        let mut width = 0;
286        for b in text.bytes() {
287            match b {
288                b'\t' | b'\n' | b'\r' => width += 1,
289                0x20..=0x7E => width += 1,
290                _ => {}
291            }
292        }
293        width
294    }
295
296    /// Fast-path width for pure printable ASCII.
297    #[inline]
298    #[must_use]
299    pub fn ascii_width(text: &str) -> Option<usize> {
300        if text.bytes().all(|b| (0x20..=0x7E).contains(&b)) {
301            Some(text.len())
302        } else {
303            None
304        }
305    }
306
307    #[inline]
308    fn is_zero_width_codepoint(c: char) -> bool {
309        let u = c as u32;
310        matches!(u, 0x0000..=0x001F | 0x007F..=0x009F)
311            || matches!(u, 0x0300..=0x036F | 0x1AB0..=0x1AFF | 0x1DC0..=0x1DFF | 0x20D0..=0x20FF)
312            || matches!(u, 0xFE20..=0xFE2F)
313            || matches!(u, 0xFE00..=0xFE0F | 0xE0100..=0xE01EF)
314            || matches!(
315                u,
316                0x00AD
317                    | 0x034F
318                    | 0x180E
319                    | 0x200B
320                    | 0x200C
321                    | 0x200D
322                    | 0x200E
323                    | 0x200F
324                    | 0x2060
325                    | 0xFEFF
326            )
327            || matches!(u, 0x202A..=0x202E | 0x2066..=0x2069 | 0x206A..=0x206F)
328    }
329
330    /// Width of a single grapheme cluster.
331    #[inline]
332    #[must_use]
333    pub fn grapheme_width(grapheme: &str) -> usize {
334        if grapheme.is_ascii() {
335            return ascii_display_width(grapheme);
336        }
337        if grapheme.chars().all(is_zero_width_codepoint) {
338            return 0;
339        }
340        if use_cjk_width() {
341            return grapheme.width_cjk();
342        }
343        // Terminal-realistic VS16 handling: most terminals render text-default
344        // emoji (Emoji_Presentation=No) at 1 cell even with VS16 appended.
345        // Strip VS16 so unicode_display_width returns the text-presentation width.
346        if !trust_vs16_width()
347            && let Some(stripped) = strip_vs16(grapheme)
348        {
349            if stripped.is_empty() {
350                return 0;
351            }
352            return unicode_display_width(&stripped) as usize;
353        }
354        unicode_display_width(grapheme) as usize
355    }
356
357    /// Width of a single Unicode scalar.
358    #[inline]
359    #[must_use]
360    pub fn char_width(ch: char) -> usize {
361        if ch.is_ascii() {
362            return match ch {
363                '\t' | '\n' | '\r' => 1,
364                ' '..='~' => 1,
365                _ => 0,
366            };
367        }
368        if is_zero_width_codepoint(ch) {
369            return 0;
370        }
371        if use_cjk_width() {
372            ch.width_cjk().unwrap_or(0)
373        } else {
374            ch.width().unwrap_or(0)
375        }
376    }
377
378    /// Width of a string in terminal cells.
379    #[inline]
380    #[must_use]
381    pub fn display_width(text: &str) -> usize {
382        if let Some(width) = ascii_width(text) {
383            return width;
384        }
385        if text.is_ascii() {
386            return ascii_display_width(text);
387        }
388        let cjk_width = use_cjk_width();
389        if !text.chars().any(is_zero_width_codepoint) {
390            if cjk_width {
391                return text.width_cjk();
392            }
393            return unicode_display_width(text) as usize;
394        }
395        text.graphemes(true).map(grapheme_width).sum()
396    }
397
398    #[cfg(test)]
399    mod tests {
400        use super::*;
401
402        // ── env helpers (testable without OnceLock) ─────────────────
403
404        #[test]
405        fn cjk_width_env_explicit_true() {
406            let get = |key: &str| match key {
407                "FTUI_GLYPH_DOUBLE_WIDTH" => Some("1".into()),
408                _ => None,
409            };
410            assert!(cjk_width_from_env(get));
411        }
412
413        #[test]
414        fn cjk_width_env_explicit_false() {
415            let get = |key: &str| match key {
416                "FTUI_GLYPH_DOUBLE_WIDTH" => Some("0".into()),
417                _ => None,
418            };
419            assert!(!cjk_width_from_env(get));
420        }
421
422        #[test]
423        fn cjk_width_env_text_cjk_key() {
424            let get = |key: &str| match key {
425                "FTUI_TEXT_CJK_WIDTH" => Some("true".into()),
426                _ => None,
427            };
428            assert!(cjk_width_from_env(get));
429        }
430
431        #[test]
432        fn cjk_width_env_fallback_key() {
433            let get = |key: &str| match key {
434                "FTUI_CJK_WIDTH" => Some("yes".into()),
435                _ => None,
436            };
437            assert!(cjk_width_from_env(get));
438        }
439
440        #[test]
441        fn cjk_width_env_japanese_locale() {
442            let get = |key: &str| match key {
443                "LC_CTYPE" => Some("ja_JP.UTF-8".into()),
444                _ => None,
445            };
446            assert!(cjk_width_from_env(get));
447        }
448
449        #[test]
450        fn cjk_width_env_chinese_locale() {
451            let get = |key: &str| match key {
452                "LANG" => Some("zh_CN.UTF-8".into()),
453                _ => None,
454            };
455            assert!(cjk_width_from_env(get));
456        }
457
458        #[test]
459        fn cjk_width_env_korean_locale() {
460            let get = |key: &str| match key {
461                "LC_CTYPE" => Some("ko_KR.UTF-8".into()),
462                _ => None,
463            };
464            assert!(cjk_width_from_env(get));
465        }
466
467        #[test]
468        fn cjk_width_env_english_locale_returns_false() {
469            let get = |key: &str| match key {
470                "LANG" => Some("en_US.UTF-8".into()),
471                _ => None,
472            };
473            assert!(!cjk_width_from_env(get));
474        }
475
476        #[test]
477        fn cjk_width_env_no_vars_returns_false() {
478            let get = |_: &str| -> Option<String> { None };
479            assert!(!cjk_width_from_env(get));
480        }
481
482        #[test]
483        fn cjk_width_env_glyph_overrides_locale() {
484            // FTUI_GLYPH_DOUBLE_WIDTH=0 should override a CJK locale
485            let get = |key: &str| match key {
486                "FTUI_GLYPH_DOUBLE_WIDTH" => Some("0".into()),
487                "LANG" => Some("ja_JP.UTF-8".into()),
488                _ => None,
489            };
490            assert!(!cjk_width_from_env(get));
491        }
492
493        #[test]
494        fn cjk_width_env_on_is_true() {
495            let get = |key: &str| match key {
496                "FTUI_GLYPH_DOUBLE_WIDTH" => Some("on".into()),
497                _ => None,
498            };
499            assert!(cjk_width_from_env(get));
500        }
501
502        #[test]
503        fn cjk_width_env_case_insensitive() {
504            let get = |key: &str| match key {
505                "FTUI_CJK_WIDTH" => Some("TRUE".into()),
506                _ => None,
507            };
508            assert!(cjk_width_from_env(get));
509        }
510
511        // ── VS16 trust from env ─────────────────────────────────────
512
513        #[test]
514        fn vs16_trust_unicode_string() {
515            let get = |key: &str| match key {
516                "FTUI_EMOJI_VS16_WIDTH" => Some("unicode".into()),
517                _ => None,
518            };
519            assert!(vs16_trust_from_env(get));
520        }
521
522        #[test]
523        fn vs16_trust_value_2() {
524            let get = |key: &str| match key {
525                "FTUI_EMOJI_VS16_WIDTH" => Some("2".into()),
526                _ => None,
527            };
528            assert!(vs16_trust_from_env(get));
529        }
530
531        #[test]
532        fn vs16_trust_not_set() {
533            let get = |_: &str| -> Option<String> { None };
534            assert!(!vs16_trust_from_env(get));
535        }
536
537        #[test]
538        fn vs16_trust_other_value() {
539            let get = |key: &str| match key {
540                "FTUI_EMOJI_VS16_WIDTH" => Some("1".into()),
541                _ => None,
542            };
543            assert!(!vs16_trust_from_env(get));
544        }
545
546        #[test]
547        fn vs16_trust_case_insensitive() {
548            let get = |key: &str| match key {
549                "FTUI_EMOJI_VS16_WIDTH" => Some("UNICODE".into()),
550                _ => None,
551            };
552            assert!(vs16_trust_from_env(get));
553        }
554
555        // ── ascii_width fast path ───────────────────────────────────
556
557        #[test]
558        fn ascii_width_pure_ascii() {
559            assert_eq!(ascii_width("hello"), Some(5));
560        }
561
562        #[test]
563        fn ascii_width_empty() {
564            assert_eq!(ascii_width(""), Some(0));
565        }
566
567        #[test]
568        fn ascii_width_with_space() {
569            assert_eq!(ascii_width("hello world"), Some(11));
570        }
571
572        #[test]
573        fn ascii_width_non_ascii_returns_none() {
574            assert_eq!(ascii_width("héllo"), None);
575        }
576
577        #[test]
578        fn ascii_width_with_tab_returns_none() {
579            // Tab (0x09) is outside 0x20..=0x7E
580            assert_eq!(ascii_width("hello\tworld"), None);
581        }
582
583        #[test]
584        fn ascii_width_with_newline_returns_none() {
585            assert_eq!(ascii_width("hello\n"), None);
586        }
587
588        #[test]
589        fn ascii_width_control_char_returns_none() {
590            assert_eq!(ascii_width("\x01"), None);
591        }
592
593        // ── char_width ──────────────────────────────────────────────
594
595        #[test]
596        fn char_width_ascii_letter() {
597            assert_eq!(char_width('A'), 1);
598        }
599
600        #[test]
601        fn char_width_space() {
602            assert_eq!(char_width(' '), 1);
603        }
604
605        #[test]
606        fn char_width_tab() {
607            assert_eq!(char_width('\t'), 1);
608        }
609
610        #[test]
611        fn char_width_newline() {
612            assert_eq!(char_width('\n'), 1);
613        }
614
615        #[test]
616        fn char_width_nul() {
617            // NUL (0x00) is an ASCII control char, zero width
618            assert_eq!(char_width('\0'), 0);
619        }
620
621        #[test]
622        fn char_width_bell() {
623            // BEL (0x07) is an ASCII control char, zero width
624            assert_eq!(char_width('\x07'), 0);
625        }
626
627        #[test]
628        fn char_width_combining_accent() {
629            // U+0301 COMBINING ACUTE ACCENT is zero-width
630            assert_eq!(char_width('\u{0301}'), 0);
631        }
632
633        #[test]
634        fn char_width_zwj() {
635            // U+200D ZERO WIDTH JOINER
636            assert_eq!(char_width('\u{200D}'), 0);
637        }
638
639        #[test]
640        fn char_width_zwnbsp() {
641            // U+FEFF ZERO WIDTH NO-BREAK SPACE
642            assert_eq!(char_width('\u{FEFF}'), 0);
643        }
644
645        #[test]
646        fn char_width_soft_hyphen() {
647            // U+00AD SOFT HYPHEN
648            assert_eq!(char_width('\u{00AD}'), 0);
649        }
650
651        #[test]
652        fn char_width_wide_east_asian() {
653            // '⚡' (U+26A1) has east_asian_width=W, always width 2
654            assert_eq!(char_width('⚡'), 2);
655        }
656
657        #[test]
658        fn char_width_cjk_ideograph() {
659            // CJK ideographs are always width 2
660            assert_eq!(char_width('中'), 2);
661        }
662
663        #[test]
664        fn char_width_variation_selector() {
665            // U+FE0F VARIATION SELECTOR-16 is zero-width
666            assert_eq!(char_width('\u{FE0F}'), 0);
667        }
668
669        // ── display_width ───────────────────────────────────────────
670
671        #[test]
672        fn display_width_ascii() {
673            assert_eq!(display_width("hello"), 5);
674        }
675
676        #[test]
677        fn display_width_empty() {
678            assert_eq!(display_width(""), 0);
679        }
680
681        #[test]
682        fn display_width_cjk_chars() {
683            // Each CJK character is width 2
684            assert_eq!(display_width("中文"), 4);
685        }
686
687        #[test]
688        fn display_width_mixed_ascii_cjk() {
689            // 'a' = 1, '中' = 2, 'b' = 1
690            assert_eq!(display_width("a中b"), 4);
691        }
692
693        #[test]
694        fn display_width_combining_chars() {
695            // 'e' + combining acute = 1 grapheme, width 1
696            assert_eq!(display_width("e\u{0301}"), 1);
697        }
698
699        #[test]
700        fn display_width_ascii_with_control_codes() {
701            // Non-printable ASCII control chars in non-pure-ASCII path
702            // Tab/newline/CR get width 1 via ascii_display_width
703            assert_eq!(display_width("a\tb"), 3);
704        }
705
706        // ── grapheme_width ──────────────────────────────────────────
707
708        #[test]
709        fn grapheme_width_ascii_char() {
710            assert_eq!(grapheme_width("A"), 1);
711        }
712
713        #[test]
714        fn grapheme_width_cjk_ideograph() {
715            assert_eq!(grapheme_width("中"), 2);
716        }
717
718        #[test]
719        fn grapheme_width_combining_sequence() {
720            // 'e' + combining accent is one grapheme, width 1
721            assert_eq!(grapheme_width("e\u{0301}"), 1);
722        }
723
724        #[test]
725        fn grapheme_width_zwj_cluster() {
726            // ZWJ alone is zero-width
727            assert_eq!(grapheme_width("\u{200D}"), 0);
728        }
729    }
730}