termimad-0.29.4/.cargo_vcs_info.json0000644000000001360000000000100127320ustar { "git": { "sha1": "203ab6803efc3f109184dfc14e5c87c59c3ab4be" }, "path_in_vcs": "" }termimad-0.29.4/.github/workflows/deny.yml000064400000000000000000000013261046102023000166030ustar 00000000000000on: [push, pull_request] name: cargo-deny jobs: deny: name: deny runs-on: ubuntu-latest strategy: matrix: checks: - advisories - bans licenses sources # Prevent sudden announcement of a new advisory from failing ci: continue-on-error: ${{ matrix.checks == 'advisories' }} steps: - name: Checkout sources uses: actions/checkout@v4.1.1 - name: Install toolchain uses: dtolnay/rust-toolchain@master with: toolchain: 1.73.0 - name: Cache uses: Swatinem/rust-cache@v2 - name: cargo-deny uses: EmbarkStudios/cargo-deny-action@v1 with: command: check ${{ matrix.checks }} termimad-0.29.4/.gitignore000064400000000000000000000000511046102023000135060ustar 00000000000000/target Cargo.lock trav .bacon-locations termimad-0.29.4/CHANGELOG.md000064400000000000000000000372361046102023000133460ustar 00000000000000*If you're reading this because you try make sense of some new API or a breaking change, you might also be interested in coming to the chat for explanations or guidance.* ### v0.29.4 - 2024-06-17 - fix compilation with `default-features = false` - Fix #62 ### v0.29.3 - 2024-06-13 - `ask!` macro doesn't need separate imports anymore ### v0.29.2 - 2024-04-24 - update Crokey to 1.0.0 ### v0.29.1 - 2024-02-10 - event source's `combining` now `false` by default ### v0.29.0 - 2024-01-29 - list items are now by default indented as blocks. It's possible to revert to the old rendering (only the first line indented) with the list_items_identation_mod field of the skin - Fix #21 ### v0.28.2 - 2024-01-26 - Better support of repeated keys in EventSource ### v0.28.1 - 2024-01-20 - EventSource by default mandates modifier (or space) for combinations with multiple simple keys. This can be changed with an option ### v0.28.0 - 2024-01-18 - Major change: termimad and its coolor and crokey dependencies now use the version 0.27 of Crossterm, which brings many breaking changes but allows new capabilities in key events handling. Termimad's EventSource now outputs key combinations along crossterm events. ### v0.27.0 - 2024-01-08 - paragraphs, code blocks, headers, and tables can be given a left_margin and a right_margin - Fix #11 ### v0.26.1 - 2023-11-05 - can_move_left and can_move_right functions on InputField ### v0.26.0 - 2023-11-03 - MadSkin and other structs now implement serde::Serialize - Fix #19 ### v0.25.7 - 2023-10-31 - upgrade coolor, removing the ansi_colours dependency, fixing a license incompatibility - Fix #51 ### v0.25.6 - 2023-10-31 - upgrade terminal-clipboard due to RUSTSEC-2021-0019 in its X11 dependency ### v0.25.5 - 2023-10-16 - dependency version updated ### v0.25.3 - 2023-10-12 - coolor updated to 0.5.1 ### v0.25.2 - 2023-09-03 - fix shift-key extending the selection ### v0.25.1 - 2023-09-01 - option for rounded corner tables in skin - see test-template example ### v0.25.0 - 2023-08-20 - `skin.limit_to_ascii()` makes the characters used for table borders, bullets, etc. be in the non extended ASCII range - see "high-compatibility" example ### v0.24.1 - 2023-08-15 - attribute parsing support RapidBlink and SlowBlink - better error reporting on parsing invalid grey level ### v0.24.0 - 2023-08-13 - gray panicking on out of range level has been changed to a silent clamping - skin now deserializable with serde - several utilities to parse colors and style elements ### v0.23.2 - 2023-08-02 - utility functions (`FmtText::content_width` and `FmtText::set_rendering_width`) and example (content-align) helping base alignments on the content's width rather than the terminal's width ### v0.23.1 - 2023-06-15 - reexport coolor to ease dependency managment ### v0.23.0 - 2023-03-09 - FmtText::raw_str function to build a text with no markdown interpretation ### v0.22.0 - 2023-03-02 - with 1, 2, or 3 spaces before a bullet, you make a deeper list item - Fix #41 ### v0.21.1 - 2023-02-25 - expose the DisplayableLine struct, which can be useful for some widgets ### v0.21.0 - 2023-02-05 - parsing markdown with options (clean indentation, continue spans); see parse-options example - Fix #38 ### v0.20.6 - 2022-12-15 - fix cases of selection becoming wider than content in input field ### v0.20.5 - 2022-12-15 - fix coolor version to prevent a dependency version problem with crossterm ### v0.20.4 - 2022-12-03 - reexport crossterm ### v0.20.3 - 2022-09-20 - fix some '\' not being rendered because being incorrectly considered as escaping ### v0.20.2 - 2022-06-07 - update crossterm to 0.23.1 and coolor to 0.5 ### v0.20.1 - 2022-04-14 - upgrade coolor to 0.4 (which is pure rust) to fix a cross-compilation problem ### v0.20.0 - 2021-12-25 - better algorithm for fitting tables, taking into account the whole column and not just its max width - some fitting functions which could panic on an assert in extrem cases now return a Result (there are unlikely to be used directly so this shouldn't lead to breaking changes) ### v0.19.4 - 2021-12-22 - fix a case of crash in input field ### v0.19.3 - 2021-12-01 - fix dimension of drawn Rect ### v0.19.2 - 2021-11-28 - better support of wide characters in InputField ### v0.19.1 - 2021-11-26 - update Crossterm to 0.22.1 to fix some bugs ### v0.19.0 - 2021-11-15 - addition of the small `Rect` utility, which lets you draw rects ### v0.18.0 - 2021-11-11 - MadSkin blend_with function allows blending a skin with a color, with a given weight, enabling for example fading a skin into its background, which can be useful for displaying behind a dialog ### v0.17.1 - 2021-11-06 - InputField: fix suppr key at end of line not joining lines ### v0.17.0 - 2021-10-29 Several event related API have changed in a breaking way in this release - EventSource now emits instances of TimedEvent, which wrap crossterm events - InputField: selection & selection based operation (cut, copy, paste) - InputField: double click selects the word around ### v0.16.4 - 2021-10-22 - Remove the need to explicitly import minimad for crates using mad_print_inline! - InputField: remove `\r` from edited strings ### v0.16.3 - 2021-10-16 - Minimad's new TableBuilder, a facility to build text templates for tables ### v0.16.2 - 2021-09-27 - insert_str function in InputField ### v0.16.1 - 2021-09-08 - Home and End key now move to start and end of lines (i.e. no more of text) in multi-lines input fields - several improvements and small fixes in input fields and scrolling - TextView and MadView don't erase right of their area anymore ### v0.16.0 - 2021-09-05 - some scroll API now use unsigned ints instead of i32 - input fields much improved. Can now be several lines - see examples/inputs - the API of InputFields changed - but adapting user code should be straighforward: - Replace `set_content(&str)` with `set_str(AsRef)` or `clear()`. - Use `set_focus(bool)` instead of the `focused` field (now private). - Use `area()` and `set_area(Area)` instead of the `area` field (now private). ### v0.15.0 - 2021-08-29 - organize and augment the utilities dedicated to writing text, formatted or not, in a limited size area - remove the Result type - upgrade crossterm to 0.21 ### v0.14.3 - 2021-08-23 - password mode in input field ### v0.14.2 - 2021-08-09 - add `MadSkin::default_dark` and `MadSkin::default_light`, two default skins suitable for specific kind of terminals ### v0.14.1 - 2021-08-06 - change default skin to ensure it's readable whatever the terminal theme ### v0.14.0 - 2021-07-07 - `ask!` macro and `Question` API - the `mad_print_inline` and `mad_write_inline` macros now accept any argument supporting `to_string()` and not just `&str` - fix a bug making tables sometimes exceed the width limit ### v0.13.0 - 2021-06-29 - support wide chars everywhere, rewritten algorithms for markdown wrapping and fitting ### v0.12.1 - 2021-06-24 - improved heuristics for table fitting & wrapping ### v0.12.0 - 2021-06-24 - upgrade crossterm to 0.20 (beware that crossterm's API changed) ### v0.11.1 - 2021-06-03 - fix some problems (including a crash) with input_field.del_word_right ### v0.11.0 - 2021-06-02 - eases imports: it's no more needed to import the lazy_static crate and no import is needed for using macros ### v0.10.3 - 2021-05-28 - fix a pb with wide chars in tables ### v0.10.2 - 2021-04-27 - consider backspace as having a col width of -1 (they move the cursor to the left when printed in terminal) ### v0.10.1 - 2021-03-18 - Fix a crash in `input_field.del_word_left()` ### v0.10.0 - 2021-02-15 - Style characters can now be escaped with a '\' - Fix #24 ### v0.9.7 - 2021-02-10 - MadSkin::no_style() builds a skin without any style information, suitable for writing in files ### v0.9.6 - 2021-02-07 - fix a bad column widths reduction ### v0.9.5 - 2021-01-31 - names of variables in templates can now contain digits - better column balancing in thight tables ### v0.9.4 - 2021-01-29 - update crossbeam dependency to 0.8 ### v0.9.3 - 2021-01-27 - new version of minimad -> owning templates allow passing any Display as value ### v0.9.2 - 2020-12-21 - styled_char and compound_style now implement Debug ### v0.9.1 - 2020-11-27 - event source intercepts escape sequences and sends them (when finished) in a dedicated channel (not reading this bounded channel is possible: escape sequences are just dropped in that case) ### v0.8.30 - 2020-11-13 - add the FitStr utility taken from broot (correct string cutting taking real char width in cols) (note that not all functions in termimad precisely take all chars width in cols into account) ### v0.8.29 - 2020-10-15 - allow default value in template expansion ### v0.8.29 - 2020-10-15 - allow default value in template expansion ### v0.8.28 - 2020-10-11 - use the OwningTemplateExpander of Minimad 0.6.6 ### v0.8.27 - 2020-10-07 - fix inverted move_left and move_to_start ### v0.8.26 - 2020-08-07 - upgrade crossterm to 0.17.7 ### v0.8.25 - 2020-07-13 - interpred lines with just ">" as empty quotes - fix panic on wrapping long strings without space ### v0.8.24 - 2020-06-22 - add a bunch of functions modifying the input (moving the cursor or deleting parts) ### v0.8.23 - 2020-05-29 - fix uppercase letters not used in input field ### v0.8.22 - 2020-05-25 - Prevent overflowing of large text from input field (some ellipsis added if necessary) ### v0.8.21 - 2020-05-13 - EventSource: better manage channel errors ### v0.8.20 - 2020-05-10 - relax the dependency version contraint on crossterm - Fix #18 ### v0.8.18 - 2020-05-05 - input fields now have a "focused" bool in their state ### v0.8.17 - 2020-02-28 - added event handling functions in input_field for when you don't use termimad events or have a complex event dispatching ### v0.8.16 - 2020-02-26 - key modifiers in click events in event_source - the new experimental feature flag: `special-renders` lets you define replacement chars (for now) with a out of skin rendering (contact me if you're interested by this kind of feature) ### v0.8.15 - 2020-02-22 - clear function in compound_style ### v0.8.14 - 2020-02-16 - check w in double-click detection ### v0.8.13 - 2020-02-08 - use crossterm 0.16.0 for slightly improved attributes storage ### v0.8.13 - 2020-01-19 - use crossterm 0.15 to fix ctrl-J being read as Enter ### v0.8.11 - 2020-01-19 - fix missing background on space after bullet in list ### v0.8.10 - 2020-01-11 - use crossterm 0.14.2 for freeBSD compatibility ### v0.8.9 - 2019-12-30 - fix the Enter key not recognized in combinations on some computers by normalizing '\r' and '\n' into 'Enter' ### v0.8.8 - 2019-12-26 - allow language specification in code fences ### v0.8.5 - 2019-12-20 - code fences support ### v0.8.4 - 2019-12-16 - fix a compilation problem on windows (see https://github.com/Canop/termimad/issues/13#issuecomment-565848039) ### v0.8.3 - 2019-12-15 - port to crossterm 0.14 ### v0.8.2 - 2019-11-29 - skin.print_expander makes using a text template less verbose ### v0.8.1 - 2019-11-29 - TextView: draw the background till the end of line ### v0.8.0 - 2019-11-24 - Templates allow going further in separating form from content ### v0.7.6 - 2019-11-15 - fix skin's background not applied on empty lines at end of text_view - use version minimad 0.4.3 to fix case of code not detected when following italic without space in between ### v0.7.5 - 2019-11-13 - fix skin's background not applied on empty lines at end of text_view ### v0.7.4 - 2019-11-11 - introduce inline templates, and especially the `mad_print_inline!` and `mad_write_inline!` macros - add functions to shrink or extend a composite to a given width, using internal elision if possible ### v0.7.3 - 2019-11-08 - add easy alternate to `skin.print_text` handling IO error ### v0.7.2 - 2019-11-04 - incorporate crossterm 0.13.2 which fixes a regression on input reader ### v0.7.1 - 2019-11-03 - compatibility with crossterm 0.13 - mouse support in stderr ### v0.7.0 - 2019-09-22 - Displaying can be done on stderr or stdout, or in a subshell ### v0.6.6 - 2019-10-05 - provide a default terminal width when the real one can't be measured ### v0.6.5 - 2019-08-31 - list view: autoscroll on selection change - list view: select_first_line & select_last_line ### v0.6.4 - 2019-08-02 - add ProgressBar ### v0.6.3 - 2019-08-01 - improvements of ListView ### v0.6.2 - 2019-07-31 - fix build inconsistencies due to lack of precise sub crate versionning in crossterm ### v0.6.1 - 2019-07-29 - add modifiable style for the input_field ### v0.6.0 - 2019-07-28 Some tools that were parts of several Termimad based applications are now shared here: - an event source emmiting events on a crossbeam channel - an input field - a list view with auto-resized columns ### v0.5.1 - 2019-07-21 - a few utilies exported for apps with specific usages (compute_scrollbar, spacing.print_str, etc.) ### v0.5.0 - 2019-07-09 - different styles for inline_code and code_block - rgb now longer a macro but a free function - two more color functions: ansi and gray - clean and complete documentation ### v0.4.0 - 2019-07-02 - support for horizontal rule (line made of dashes) - support for quote (line starting with '>') - support for bullet style customization (including colors) - better wrapping, less frequently breaks words - Skin API *breaking changes* to allow for more customization termimad-0.29.4/Cargo.lock0000644000000612070000000000100107130ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" dependencies = [ "memchr", ] [[package]] name = "android-tzdata" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" [[package]] name = "android_system_properties" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" dependencies = [ "libc", ] [[package]] name = "anyhow" version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" [[package]] name = "block" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] name = "bumpalo" version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" [[package]] name = "cc" version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "libc", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", "wasm-bindgen", "windows-targets", ] [[package]] name = "cli-log" version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d2ab00dc4c82ec28af25ac085aecc11ffeabf353755715a3113a7aa044ca5cc" dependencies = [ "chrono", "file-size", "log", "proc-status", ] [[package]] name = "clipboard-win" version = "4.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" dependencies = [ "error-code", "str-buf", "winapi", ] [[package]] name = "clipboard_macos" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "145a7f9e9b89453bc0a5e32d166456405d389cea5b578f57f1274b1397588a95" dependencies = [ "objc", "objc-foundation", "objc_id", ] [[package]] name = "coolor" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e93977247fb916abeee1ff8c6594c9b421fd9c26c9b720a3944acb2a7de27b" dependencies = [ "crossterm", ] [[package]] name = "core-foundation-sys" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "crokey" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b48209802ec5862bb034cb16719eec24d1c759e62921be7d3c899d0d85f3344b" dependencies = [ "crokey-proc_macros", "crossterm", "once_cell", "serde", "strict", ] [[package]] name = "crokey-proc_macros" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397d3c009d8df93c4b063ddaa44a81ee7098feb056f99b00896c36e2cee9a9f7" dependencies = [ "crossterm", "proc-macro2", "quote", "strict", "syn 1.0.109", ] [[package]] name = "crossbeam" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c" dependencies = [ "cfg-if", "crossbeam-channel", "crossbeam-deque", "crossbeam-epoch", "crossbeam-queue", "crossbeam-utils", ] [[package]] name = "crossbeam-channel" version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-deque" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if", "crossbeam-epoch", "crossbeam-utils", ] [[package]] name = "crossbeam-epoch" version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", "memoffset 0.9.0", "scopeguard", ] [[package]] name = "crossbeam-queue" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if", ] [[package]] name = "crossterm" version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df" dependencies = [ "bitflags 2.4.2", "crossterm_winapi", "libc", "mio", "parking_lot", "signal-hook", "signal-hook-mio", "winapi", ] [[package]] name = "crossterm_winapi" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" dependencies = [ "winapi", ] [[package]] name = "deser-hjson" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4fe3d77c19507c98946cbc1077ee3c1b50222b4aafcd248e40f4137913ad98" dependencies = [ "serde", ] [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "error-code" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" dependencies = [ "libc", "str-buf", ] [[package]] name = "file-size" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9544f10105d33957765016b8a9baea7e689bf1f0f2f32c2fa2f568770c38d2b3" [[package]] name = "gethostname" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb65d4ba3173c56a500b555b532f72c42e8d1fe64962b518897f8959fae2c177" dependencies = [ "libc", "winapi", ] [[package]] name = "iana-time-zone" version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", "wasm-bindgen", "windows", ] [[package]] name = "iana-time-zone-haiku" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ "cc", ] [[package]] name = "itoa" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "js-sys" version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] [[package]] name = "lazy-regex" version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e723bd417b2df60a0f6a2b6825f297ea04b245d4ba52b5a22cb679bdf58b05fa" dependencies = [ "lazy-regex-proc_macros", "once_cell", "regex", ] [[package]] name = "lazy-regex-proc_macros" version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0f0a1d9139f0ee2e862e08a9c5d0ba0470f2aa21cd1e1aa1b1562f83116c725f" dependencies = [ "proc-macro2", "quote", "regex", "syn 2.0.38", ] [[package]] name = "libc" version = "0.2.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" [[package]] name = "lock_api" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", ] [[package]] name = "log" version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "malloc_buf" version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" dependencies = [ "libc", ] [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" [[package]] name = "memoffset" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" dependencies = [ "autocfg", ] [[package]] name = "memoffset" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] [[package]] name = "minimad" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6c4610f430e49b882fcaad0186134150d4d74fc76080b0a61f7000460c2e268" dependencies = [ "once_cell", ] [[package]] name = "mio" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", "wasi", "windows-sys", ] [[package]] name = "nix" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" dependencies = [ "bitflags 1.3.2", "cfg-if", "libc", "memoffset 0.7.1", ] [[package]] name = "num-traits" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" dependencies = [ "autocfg", ] [[package]] name = "objc" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ "malloc_buf", ] [[package]] name = "objc-foundation" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" dependencies = [ "block", "objc", "objc_id", ] [[package]] name = "objc_id" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" dependencies = [ "objc", ] [[package]] name = "once_cell" version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "parking_lot" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", ] [[package]] name = "parking_lot_core" version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", "windows-targets", ] [[package]] name = "pretty_assertions" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ "diff", "yansi", ] [[package]] name = "proc-macro2" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] [[package]] name = "proc-status" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0e0c0ac915e7b76b47850ba4ffc377abde6c6ff9eeace61d0a89623db449712" dependencies = [ "thiserror", ] [[package]] name = "quote" version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] [[package]] name = "redox_syscall" version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ "bitflags 1.3.2", ] [[package]] name = "regex" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aaac441002f822bc9705a681810a4dd2963094b9ca0ddc41cb963a4c189189ea" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5011c7e263a695dc8ca064cddb722af1be54e517a280b12a5356f98366899e5d" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" [[package]] name = "ryu" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "serde" version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", "syn 2.0.38", ] [[package]] name = "serde_json" version = "1.0.108" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b" dependencies = [ "itoa", "ryu", "serde", ] [[package]] name = "signal-hook" version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" dependencies = [ "libc", "signal-hook-registry", ] [[package]] name = "signal-hook-mio" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" dependencies = [ "libc", "mio", "signal-hook", ] [[package]] name = "signal-hook-registry" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "smallvec" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "str-buf" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" [[package]] name = "strict" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f42444fea5b87a39db4218d9422087e66a85d0e7a0963a439b07bcdf91804006" [[package]] name = "syn" version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "syn" version = "2.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e96b79aaa137db8f61e26363a0c9b47d8b4ec75da28b7d1d614c2303e232408b" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "termimad" version = "0.29.4" dependencies = [ "anyhow", "cli-log", "coolor", "crokey", "crossbeam", "deser-hjson", "lazy-regex", "minimad", "pretty_assertions", "serde", "serde_json", "terminal-clipboard", "thiserror", "unicode-width", ] [[package]] name = "terminal-clipboard" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e0fd8cb5cf744b501e657eb27df7909ff917eacbfee34bc4bb13d4e6411a131" dependencies = [ "clipboard-win", "clipboard_macos", "once_cell", "termux-clipboard", "x11-clipboard", ] [[package]] name = "termux-clipboard" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f6aff13ca3293315b94f6dbd9c69e1c958fe421c294681e2ffda80c9858e36f" [[package]] name = "thiserror" version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", "syn 2.0.38", ] [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", "syn 2.0.38", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", "syn 2.0.38", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-wsapoll" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" dependencies = [ "winapi", ] [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ "windows-targets", ] [[package]] name = "windows-targets" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "x11-clipboard" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b41aca1115b1f195f21c541c5efb423470848d48143127d0f07f8b90c27440df" dependencies = [ "x11rb", ] [[package]] name = "x11rb" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1641b26d4dec61337c35a1b1aaf9e3cba8f46f0b43636c609ab0291a648040a" dependencies = [ "gethostname", "nix", "winapi", "winapi-wsapoll", "x11rb-protocol", ] [[package]] name = "x11rb-protocol" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82d6c3f9a0fb6701fab8f6cea9b0c0bd5d6876f1f89f7fada07e558077c344bc" dependencies = [ "nix", ] [[package]] name = "yansi" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" termimad-0.29.4/Cargo.toml0000644000000032040000000000100107270ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.56" name = "termimad" version = "0.29.4" authors = ["dystroy "] description = "Markdown Renderer for the Terminal" readme = "README.md" keywords = [ "markdown", "terminal", "tui", "renderer", "parser", ] categories = [ "command-line-interface", "text-processing", "template-engine", ] license = "MIT" repository = "https://github.com/Canop/termimad" resolver = "1" [dependencies.coolor] version = "0.9.0" features = ["crossterm"] [dependencies.crokey] version = "1.0.0" [dependencies.crossbeam] version = "0.8" [dependencies.lazy-regex] version = "3" [dependencies.minimad] version = "0.13.0" [dependencies.serde] version = "1.0" features = ["derive"] [dependencies.thiserror] version = "1.0" [dependencies.unicode-width] version = "0.1.11" [dev-dependencies.anyhow] version = "1.0" [dev-dependencies.cli-log] version = "2" [dev-dependencies.deser-hjson] version = "2" [dev-dependencies.pretty_assertions] version = "1.4" [dev-dependencies.serde_json] version = "1" [dev-dependencies.terminal-clipboard] version = "0.4.1" [features] default = ["special-renders"] special-renders = [] termimad-0.29.4/Cargo.toml.orig000064400000000000000000000021201046102023000144040ustar 00000000000000[package] name = "termimad" version = "0.29.4" authors = ["dystroy "] repository = "https://github.com/Canop/termimad" description = "Markdown Renderer for the Terminal" edition = "2021" keywords = ["markdown", "terminal", "tui", "renderer", "parser"] license = "MIT" categories = ["command-line-interface", "text-processing", "template-engine"] readme = "README.md" rust-version = "1.56" resolver = "1" [features] special-renders = [] default = ["special-renders"] [dependencies] coolor = { version="0.9.0", features=["crossterm"] } crokey = "1.0.0" crossbeam = "0.8" lazy-regex = "3" minimad = "0.13.0" serde = { version = "1.0", features = ["derive"] } thiserror = "1.0" unicode-width = "0.1.11" # cli-log = "2.0" [dev-dependencies] anyhow = "1.0" cli-log = "2" deser-hjson = "2" pretty_assertions = "1.4" serde_json = "1" terminal-clipboard = "0.4.1" [patch.crates-io] # coolor = { path = "../coolor" } # crokey = { path = "../crokey" } # crossterm = { path = "../crossterm" } # minimad = { path = "../minimad" } # terminal-clipboard = { path = "../terminal-clipboard" } termimad-0.29.4/LICENSE000064400000000000000000000020461046102023000125310ustar 00000000000000MIT License Copyright (c) 2019 Canop 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. termimad-0.29.4/README.md000064400000000000000000000260111046102023000130010ustar 00000000000000[![MIT][s2]][l2] [![Latest Version][s1]][l1] [![docs][s3]][l3] [![Chat on Miaou][s4]][l4] [s1]: https://img.shields.io/crates/v/termimad.svg [l1]: https://crates.io/crates/termimad [s2]: https://img.shields.io/badge/license-MIT-blue.svg [l2]: LICENSE [s3]: https://docs.rs/termimad/badge.svg [l3]: https://docs.rs/termimad/ [s4]: https://miaou.dystroy.org/static/shields/room.svg [l4]: https://miaou.dystroy.org/3 A CLI utilities library leveraging Markdown to format terminal rendering, allowing separation of structure, data and skin. Based on [crossterm](#crossterm-compatibility) so works on most terminals (even on windows). ![text](doc/text.png) The goal isn't to display any markdown text with its various extensions (a terminal isn't really fit for that). The goal is rather to improve the display of texts in a terminal application when we want both the text and the skin to be easily configured. Termimad also includes a few utilities helping efficient managing of events and user input in a multithread application. **Wrapping**, table balancing, and **scrolling** are essential features of Termimad. A text or a table can be displayed in an *a priori* unknown part of the screen, scrollable if desired, with a dynamically discovered width. For example this markdown: |:-:|:-:|- |**feature**|**supported**|**details**| |-:|:-:|- | tables | yes | pipe based, with or without alignments | italic, bold | yes | star based | | inline code | yes | `with backquotes` (it works in tables too) | code bloc | yes |with tabs or code fences | syntax coloring | no | | crossed text | ~~not yet~~ | wait... now it works `~~like this~~` | horizontal rule | yes | Use 3 or more dashes (`---`) | lists | yes|* unordered lists supported | | |* ordered lists *not* supported | quotes | yes |> What a wonderful time to be alive! | links | no | (but your terminal already handles raw URLs) |- will give different results depending on the width: ![table](doc/table-in-80.png) ![table](doc/table-in-60.png) ![table](doc/table-in-50.png) ## Usage ```toml [dependencies] termimad = "0.20" ``` ### With the default skin: ```rust termimad::print_inline("**some** *nested **style*** and `some(code)`"); ``` or ```rust print!("{}", termimad::inline("**some** *nested **style*** and `some(code)`")); ``` Result: ![simple example](doc/default-skin-simple.png) ### Inline snippets with a custom skin: *Inline snippets* are one line or less. ```rust let mut skin = MadSkin::default(); skin.bold.set_fg(Yellow); skin.print_inline("*Hey* **World!** Here's `some(code)`"); skin.paragraph.set_fgbg(Magenta, rgb(30, 30, 40)); skin.italic.add_attr(Underlined); println!("\nand now {}\n", skin.inline("a little *too much* **style!** (and `some(code)` too)")); ``` Result: ![too much style](doc/too_much.png) #### Texts *Texts* can be several lines. Tables and code blocks are automatically aligned, justified and consistently wrapped. ```rust skin.print_text("# title\n* a list item\n* another item"); ``` ### Scrollable TextView in a raw terminal: ![scrollable](doc/scrollable.png) The code for this example is in examples/scrollable. To read the whole text just do cargo run --example scrollable ### Templates In order to separate the rendering format from the content, you may want to have some constant markdown and fill some placeholders with dynamic items. The `format!` macro is not always a good solution for that because you may not be sure the content is free of characters which may mess the markdown. A solution is to use one of the templating functions or macros. A template is to markdown what a prepared statement is to SQL: interpreted once and preventing the content to be interpreted as parts of the structure. #### Inline Templates Example: ``` mad_print_inline!( &skin, "**$0 formula:** *$1*", // the markdown template, interpreted once "Disk", // fills $0 "2*π*r", // fills $1. Note that the stars don't mess the markdown ); ``` ![mad_print_inline](doc/mad_print_inline.png) Main difference with using `skin.print_inline(format!( ... ))` to build some markdown and parse it: * the markdown parsing and template building are done only once (using `once_cell` internally) * the given values aren't interpreted as markdown fragments and don't impact the style * arguments can be omited, repeated, given in any order * no support for fmt parameters or arguments other than `&str` *(in the current version)* Inline templates are especially convenient combined with automated expansion or ellipsis, for filling a field in a terminal application. You'll find more examples and advice in the *inline-template* example. #### Text Template When you want to fill a multi-line area, for example the help page of your terminal application, you may use a text template. A template defines placeholders as `${name}` which you may fill when using it. For example ``` let text_template = TextTemplate::from(r#" # ${app-name} v${app-version} It is *very* ${adj}. "#); let mut expander = text_template.expander(); expander .set("app-name", "MyApp") .set("adj", "pretty") .set("app-version", "42.5.3"); skin.print_expander(expander); ``` This would render like this: ![text_template_01](doc/text_template_01.png) The values you set with `set` aren't parsed as markdown, so they may freely contain stars or backquotes. A template is reusable and can be defined from a text content or any string. By using *sub-templates*, you may handle repetitions. They're handy for lists or tables. For example ``` let text_template = TextTemplate::from(r#" |:-:|:-:|:-:| |**name**|**path**|**description**| |-:|:-:|:-| ${module-rows |**${module-name}**|`${app-version}/${module-key}`|${module-description}| } |-|-|-| "#); let mut expander = text_template.expander(); expander .set("app-version", "2"); expander.sub("module-rows") .set("module-name", "lazy-regex") .set("module-key", "lrex") .set("module-description", "eases regexes"); expander.sub("module-rows") .set("module-name", "termimad") .set("module-key", "tmd") .set_md("module-description", "do things on *terminal*"); skin.print_expander(expander); ``` to get ![text_template_02](doc/text_template_02.png) On this example, you can note that * `sub("module-rows")` gets an expander for the sub template called `module-rows` * `set_md` can be used when you want to insert not just a raw uninterpreted string but some inline markdown. * you don't need to fill global placeholders again (here `${app-version}`). If you want to insert a block of code, you may use `set_lines` which applies the line style to all passed lines. For example ``` let text_template = TextTemplate::from(r#" ## Example of a code block ${some-function} "#); let mut expander = text_template.expander(); expander.set_lines("some-function", r#" fun test(a rational) { irate(a) } "#); skin.print_expander(expander); ``` to get ![text_template_03](doc/text_template_03.png) You'll find more text template functions in the documentation and in the example (run `cargo run --example text-template`). You may also be interested in `OwningTemplateExpander`: an alternative expander owning the values which may be handy when you build them while iterating in sub templates. ### Asking questions A frequent need in CLI apps is to ask the user to select an answer. The `Question` API and the `ask!` macros cover most basic needs. Here's an example of asking with a default choice (that you get by hitting *enter*) and a returned value: ```rust let choice = ask!(skin, "Do you want to drink something ?", ('n') { ('w', "I'd like some **w**ater, please.") => { mad_print_inline!(skin, "*Wait a minute, please, I'll fetch some.*\n"); Some("water") } ('b', "Could I get a **b**eer glass ?") => { mad_print_inline!(skin, "We have no glass, so here's a *bottle*.\n"); Some("beer") } ('n', "*No*, thank you.") => { None } }); dbg!(choice); ``` ![ask example](doc/ask.png) ### Skin Files A `MadSkin` can be deserialized using serde. For example, such a skin in Hjson could be ```Hjson bold: "#fb0 bold" italic: dim italic strikeout: crossedout red bullet: ○ yellow bold paragraph: gray(20) code_block: gray(2) gray(15) center headers: [ yellow bold center yellow underlined yellow ] quote: > red horizontal-rule: "~ #00cafe" table: "#540 center" scrollbar: "#fb0 gray(11) |" ``` Execute `cargo run --example skin-file` for an example and explanations. ## Advices to get started * Start by reading the examples (in `/examples`): they cover almost the whole API, including templates, how to use an alternate screen or scroll the page, etc. Many examples print a bunch of relevant documentation. * The render-input-markdown example lets you type some markdown in a text area and see it rendered below * Be careful that some colors aren't displayable on all terminals. The default color set of your application should not include arbitrary RGB colors. * The event / event-source part of Termimad is currently tailored for a short number of applications. If you use it or want to use it, please come and tell me so that your needs are taken into account! * If your goal is to format some CLI application output, for example a few tables, have a look at [dysk](https://github.com/Canop/dysk) which is one of the simplest possible uses * If a feature is missing, or you don't know how to use some part, come and ping me on [my chat](https://miaou.dystroy.org/3768) during West European hours. ## Open-source applications using termimad * [broot](https://github.com/Canop/broot) is a file manager and uses termimad for its help screen, status information and event management * [lfs](https://github.com/Canop/lfs) is a linux utility displaying file systems. Termimad templates are used to show the data in tables * [SafeCloset](https://github.com/Canop/safecloset) is a secret safe. Its TUI uses Termimad a lot, especially inputs * [Rhit](https://github.com/Canop/rhit) is a nginx log analyzer. Termimad templates are used to show the data in tables * [bacon](https://github.com/Canop/bacon) is a background Rust compiler. It uses Termimad for display and event management * [lapin](https://github.com/Canop/lapin) is a terminal game. It uses Termimad for display and event management * [backdown](https://github.com/Canop/backdown) is a file deduplicator. It uses Termimad to print on screen and ask questions * [Humility](https://github.com/oxidecomputer/humility) is a debugger for embedded systems. It uses Termimad to print Markdown documentation with the `humility doc` subcommand If you're the author of another application using Termimad, please tell me. ## Crossterm compatibility [Crossterm](https://github.com/crossterm-rs/crossterm) is a 0.x library which means its API isn't frozen. And it does change sometimes so libraries based on Crossterm can't always use its last version. Crossterm 0.27.x is reexported by Termimad so you don't have to declare the import yourself. You may use crossterm as `termimad::crossterm`. termimad-0.29.4/bacon.toml000064400000000000000000000041431046102023000135030ustar 00000000000000# This is a configuration file for the bacon tool # # Bacon repository: https://github.com/Canop/bacon # Complete help on configuration: https://dystroy.org/bacon/config/ default_job = "check-all" [jobs.check] command = ["cargo", "check", "--color", "always"] need_stdout = false [jobs.check-no-features] command = ["cargo", "check", "--no-default-features", "--color", "always"] need_stdout = false [jobs.check-all] command = ["cargo", "check", "--all-targets", "--color", "always"] need_stdout = false watch = ["tests", "benches", "examples"] [jobs.clippy] command = [ "cargo", "clippy", "--all-targets", "--color", "always", "--", "-A", "clippy::match_like_matches_macro", "-A", "clippy::manual_range_contains", ] need_stdout = false watch = ["tests", "benches", "examples"] [jobs.test] command = ["cargo", "test", "--color", "always"] need_stdout = true watch = ["tests"] [jobs.ex] command = ["cargo", "run", "--color", "always", "--example"] allow_warnings = true need_stdout = true [jobs.doc] command = ["cargo", "doc", "--color", "always", "--no-deps"] need_stdout = false # If the doc compiles, then it opens in your browser and bacon switches # to the previous job [jobs.doc-open] command = ["cargo", "doc", "--color", "always", "--no-deps", "--open"] need_stdout = false on_success = "back" # so that we don't open the browser at each change # You can run your application and have the result displayed in bacon, # *if* it makes sense for this crate. You can run an example the same # way. Don't forget the `--color always` part or the errors won't be # properly parsed. # If you want to pass options to your program, a `--` separator # will be needed. [jobs.run] command = [ "cargo", "run", "--color", "always" ] need_stdout = true allow_warnings = true # You may define here keybindings that would be specific to # a project, for example a shortcut to launch a specific job. # Shortcuts to internal functions (scrolling, toggling, etc.) # should go in your personal prefs.toml file instead. [keybindings] a = "job:check-all" i = "job:initial" c = "job:clippy" d = "job:doc-open" t = "job:test" r = "job:run" termimad-0.29.4/deny.toml000064400000000000000000000045241046102023000133630ustar 00000000000000[licenses] # The lint level for crates which do not have a detectable license unlicensed = "deny" # List of explicitly allowed licenses # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.7 short identifier (+ optional exception)]. allow = [] # List of explicitly disallowed licenses # See https://spdx.org/licenses/ for list of possible licenses # [possible values: any SPDX 3.7 short identifier (+ optional exception)]. deny = [] # The lint level for licenses considered copyleft copyleft = "deny" # Blanket approval or denial for OSI-approved or FSF Free/Libre licenses # * both - The license will only be approved if it is both OSI-approved *AND* FSF/Free # * either - The license will be approved if it is either OSI-approved *OR* FSF/Free # * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF/Free # * fsf-only - The license will be approved if is FSF/Free *AND NOT* OSI-approved # * neither - The license will be denied if is FSF/Free *OR* OSI-approved allow-osi-fsf-free = "either" # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the # canonical license text of a valid SPDX license file. # [possible values: any between 0.0 and 1.0]. confidence-threshold = 0.8 exceptions = [ ] [bans] # Lint level for when multiple versions of the same crate are detected multiple-versions = "warn" # The graph highlighting used when creating dotgraphs for crates # with multiple versions # * lowest-version - The path to the lowest versioned duplicate is highlighted # * simplest-path - The path to the version with the fewest edges is highlighted # * all - Both lowest-version and simplest-path are used highlight = "all" # List of crates that are allowed. Use with care! allow = [ ] # List of crates to deny deny = [ # Each entry the name of a crate and a version range. If version is # not specified, all versions will be matched. ] # Certain crates/versions that will be skipped when doing duplicate detection. skip = [ ] # Similarly to `skip` allows you to skip certain crates during duplicate detection, # unlike skip, it also includes the entire tree of transitive dependencies starting at # the specified crate, up to a certain depth, which is by default infinite skip-tree = [ ] [advisories] ignore = [ ] termimad-0.29.4/doc/ask.png000064400000000000000000000333231046102023000135570ustar 00000000000000PNG  IHDRDsBIT|dtEXtSoftwaregnome-screenshot>.tEXtCreation TimeWed 07 Jul 2021 09:07:18 PM CESTg IDATxy\TMYuX\ \5qRs悙(]r_*i.(j\HٌtX~##0 |>~d{~YcԾ}b@1 A)b@|2d0+W~r#F '8xQ[|=)/bĈU7CѰaCBCz~ŲeKb踞8ZlIh!-_qCajb*̜9;~d_=ԯ_xP!Ι3?Edd$})={0d\ǐSNu6CbkIFF_>S<<>gRPPB୷0p9O,Yd9tԑ(7VVV̘1:Dž |$spAgΜAֲC -[ΥKILLd/iydxqp(QQٳWQULMMyE_6lȵk1;vOTw9qȑjcRԫWkkkbccⰴ~dggK6F@?M6%%%ݻ'k~m\z_A022bϞ=hξ}pssZj>ٜQOyyyvRZ(JLLLXɱ¼WT~7o11ܼySCC YڵkI.\H}Uz9ԭ[e*^wppaggg?ʟRTc%$$ФIc=z4gϞe۶u^Y=D{_F[~Fff&gϞׇݓ~vd %_rRFqjsHJJf̘fΜ()**fm!7]j"JE`D9rNu_z#ؾ};vvӧOoS 6\}8\BUT.ѿ7nDtt#oI'qqqdeeKVVnnnrWWWrrrɑPa7a ԭ[W]T*eͿ5k$%%-|Yf/ allږ7䤞\DEE?Q#,WTaذ166Q#gzSl5ə3g cڴd./<ooohvd6~R=7lʊ okM:5t=?A۶mm_~];OmDŽKvۑx_W(..Ȓs1iR Ǐ###;~bWuZ֬Yĉ̚n,oW^͌ٱG ;B˖-d/i4iD֓7AFFzXj5_QF%o$7\??lU뛟BAP$IOOر}ڵ9s:V ..{ѺukWF:1MWp""""ojIc„4hЀ:ZyҒݻؿm4)<5iӆΝ;ciiI˖-۷mQ+'BѸq#VZMrrJm4)<5ԭ[IQ*ܼy[S멤VqF1 A)C =T-%^ھyatٳ[{#*.@hGn$D9U}II)'''cc]iy9T}|>TmR9R*šp|3ИGאZrr&՗˗N_̟;_JP,_H^T#X-+]V92*jT)T4q60i#3⮗h xXW&E͛ϐ!O@@eKm}{W\UV@`'X^Z[Tk?Ell,t֭RߣL23t2˗ɮ]`gg'W  --M~]6oLP$V^h R*V7륎(0T42ȉvXfΟ%ޠ[Zr[SP"غu˖-!88D-9'~h5ѿV .GIUԚ>΂Ը*,фpR`]_C# ]Gp+ERRdLLLP*jL 9KG)ajAAaDR1y|٧9͛(6l87nܨbi R_x3;w&$$/X_ :ė z/GjF!*U1{+)K(|?|Nf޼dffM@NȉOo%S(/*T~l}%}uW+rRZZe{{Zp="ޣO/ު5?({R@ڬ ꮪTrS%?$ *G֓*5ͱw6jJZTTDLL ޾LyX*W@AZ7or'fD\Ox4nnU Xk"e̚5uO __7m9>PgeG*~9ѶuςXj Vk׭[[UK3st~T^a6A6bUt4{LdQWUʰGFF+wͰah̅/Vp@UG[r#k*?l7 0pRTOe 1̌ݻei˗~w< {,_˗#$o>~Eӓ"'mzѣE#|bA @P! A)b@Rd b,Y̰aCV8 bFWg3Sc 672t(F^e<Xm;iժy>}/XXQ < n%O`_z3K/JWH \ʥ8p3g kYnȾ2d`v{:FFFtԑoˣJ.vL³rRH J֘7YR@c'SNoHȒG-ʯ \uՕ={rj-/Ў=SNyڰa#K,‚w%iTJ̚⋾?v-kb֭M4jGz1ir*Q1*^@Vtk?33 `+}rҨ3!!lGVUP(ppp> X6`VTbjj~BB [P}e."2r\>m[`1XAr=gϲmۏ:/+G.yEk圜hذ>h_3SZ0+ţք|W[Tǀ;T*y&;w Sȯ@.b@RQ J@ "K+Y/1b#Ĉʹ@ Y*!{\;V+Mt^0@=c wë-[Tcq`gr!nqF߅í8);nDV ZILz, 7̤yXGGR|GvcccvA݁)f׮_ؽWOF:u4+JOldӆN{;ɻ4q665#q dѺhTvzk @dUtzScSۖ+X8paaaXXX0jHu_~!C^C*X}fuhP߾FbáDEEg^Zj.۵k7X"ݹg:uꨱcLjGR|CN~/1eTn߾-;݋pT*P"ʕ+l޼G'brZEE_L:5saz')4u&:bҳ20V\#z 45. O/>;;[mCy4h޼9rMb # MB O:!>dew?)8:hȍcn<$-(**Fn>h~3f,3g. fj֯R #G)LΞ=G'ݻŋ$I@4`% KR2S[4.=th֞P4汶zB^~hBB7JC+foooBCpM vAmcƁ;}J7Jě9Ȼ{glŽ3[r|~~غu dvZ1jj]gQ7x" . 8x!W\ ؽ}۶ܹ߮(ħ5kVӸq#6n;6t8_T1 A) (6бA{@Phmm͊ٷoL@ <!CcOLԄ 2d\@PSNv86lHݰ0dl@PĄ(  @ J/Az@tqiشiSHKKt!@ xQ[Pv튏7 6G%77א AСƥKXb!ZGn@ "D@ (E @P1 A)2 /bĈɞ= =Dh!4ibp;tϐ!YZ~]ӭWY2zu:S<<9sJW &MB@TGI׮^̞)ܽ{g-c`aaQ#e66l߾wgʔ6cqĒW]۷zj×_`ȐLIIDATP 43Mڷo3:u֎I0h@bӖAұcGMرضGAN7RbccK`$n+RBiؔzfXk.%F0L%OY2 R&lWLM.;yY?1ez*/Ƒ; -:#Yw+N^;Ϳ*}~tcƼWWN&k0|WWWzɡC+-W$&&ٙhܸ1JP( ˖-ҥ$&&|oN8}8|8hKV-evPΝ?өSGuc֭s?V5߱cLjGR_ʕ+l޼ɪԔ_eÆ\ñc9~@:?$%v( @Iajj77~Ell,aaG8qwʯ ILPD(w#&fXЭc9}w3=4uР33ZUyHr'kGR3+{WG9n|Z*tM|C__2e*oߖ{9qȑJQ2Jݽ9JNсulYū<wgggktssZj|Lx1/̌b:w̌%@5_qԫWkkkbccqqqXZZR~}֫T*155%>>^=-!!&Mˊ FH"֮]ßOrE ͟J(J`|ƒt33 0V\#Q]nj\T)gpV*TYiR #JZ׆.oW@:V%'4obbby3zhΞ=˶m?V9Ibb"9^Jaa!ؐTҡ ΍?Y[PPu?|Nf޼dffM@<22,PPP{K}+*xEJի /ts΄ 9xr}?x?<u|*C*0ˆ^nMiՐ{{dAY!&&y%)JUէԷGaЈJf`٫*HT*&O>#Gyw:׸q#eU(rss};/.DEEqj]t!77;gT-|,k֬<?~ݻӽ{w;u|dee榞JNN"6QTT1ט.'?YYY}燇mڴaݻOֲ*C3~8qzЯ_?Շnr˃ɉ-iSG hRK.ckkK߾}dGRva^ؘFիpjHqq1p툭 =Zvž c9Ȼ{glŽ3[r[dHVhJ:@&|Μ9ChhӦ}qoل .P7d/v:fΜΪU+g}nݺ|K.g *ɔ)Se'???4nܘws9~8?UvH˯kɶm?HX{ի1c:;vHrr2aaGhٲPZ)LY=|'r''{(9O.^MI'C W: )*.b`˝J*M&Vw?LM$l?m}iߴr~o.^$k&kժ|FFɷnN2*Cb iР/0t(BP iӆΝ;ciiI˖-۷O0E/uaҤ@J%7od9yV@P8dRĀ(h  4Pk']ZL8: dbu6)jJ,o" ]~jR*5m1` LMMٿo7tX+VqqqzЌ?^zѰaC23osAfz]f>>>LΝS .w&Rw&p"rssiR$$d iiik|F=O/2o| y|.?&P:5v+p-'_xx8Y~C9&ЕM&Lxׯ딟E $shFFF4nܘ^Lti~^b߾=|&^{_2T5 ??/fffh$K RT\ >?9/*((o|HT~4k֌k4㇑C,@N>K.| vvvܺu/o{:u *?suڥ;osrr2DFFbggGbb" 4lP=Oxx8 %B[lCP4Չ@ -dm۶4k֌ifhLZ? 0r̪ڗʟT|r(BɶL ۷ظ+07%%m+MBPo~֯pJԩ>ʝ;YtwgϞ=:S ,_M `_4ڵ눈`Mb5z/k#44K*Y%K/siR믭]?R7nhw)Jr6ld 3kBJyܿ_=9dLLLJۡ\ T}}W~+py̪ڗʟԇ//ՅȫN ޭ#\|WWW}{#""W^յY?T%H (gees2x<ɓxtЁy>gǎX~-wLMK^ ʔ}QEJT6###ԇCP}gÚB(#=#PuttkW/z%{} -~UOj5quԄ6++,bbb155cbbbS"&UZ 5+ uW էk֬j.ONNrp(;}FjˏRWvR돌X^vy 3fLgPT̜9'QXX 7f\(,,$#*@Cܽ{.]^Bڿ8y)*p̹s?ĉ5~5j_ʿQKG #ͫ,wuu%=]򪈋g<]lE@,վ_2WFjj :u̹s'K~iiiдiʐ"G9|ԯ_o7cff>G ///|}}psscܸ9rz,rZXXп?vQ!2ˡY}L:ݻѦMoUh^z|Tu늇#F gР;"j_݅# -[wߑ>b޹ŵk1{Ǘ_._ho};w^WTTĹsy9foYnnnh&Oǂ Y|'O"**J/gvfذa4kŋY;"J ,x/eM\aߦM_k<Zg#4lݻGݺux ȑ7* {{;NB:uHMMeŊ: 8Rdff2eGڵk{7»!%H>R?j.\d7ptt$77/f̓6}ro߾}Fehn:@`I˗~uCccc~3֭'44kYI>}^ҸIt`ddڵkXn=Gh!}rD7>o{4n~VX <==)**) jbBBP%fe<>DC! g'b@@1 A)OĀdbY ><65ٳ'ݻw㫯=>@?̀(1f[[[brݹ|9yMRH9ImS@l!޸q|U!ٱ'&bjj{ˤrU L'|,Y۷쳹xz>ǒ%y!k/zk/ ZgΜAn70+) s\~pByZudV%0ÒX2/dgND:;;i-/OUS@l؜CťMRTTDZZ 2kkk+y; ]vWWWƍ{G+)W fbE@;O!={:uܸt+V<J "kKiaQ qZ xRy"2? {̟Z9d~iփ4!r`@ "1 A)b@Ro&(h!cw>}zTV>@ xQٜ={2@ 0O?{*@ 0(bdU6m1 ʆ F  ƀAC1 *߹s !5kFnn.)))G  ƀy"C#Cc@ž} @ رcG  z@[.e@"@P A)A][\)IENDB`termimad-0.29.4/doc/default-skin-simple.png000064400000000000000000000072001046102023000166510ustar 00000000000000PNG  IHDRO7sBITOtEXtSoftwareShutterc IDATxZyXW/!!M+ťV"-e^޶3LoZպԭVq f߾xv_y{λ}OH pv<ݐJOَ #Z;(BU6-+"LŌY?^3!]a֓,>|9SFYHocF%aۢ@Tu˗KLr;fUKڧĉ'ք0aźyQΪKot0A[kK]CeRl|@n <n@rVvZ\k)>y^w߈6GSYiz6*J'09kܸ!h;:|4b~C=Uw@Su6oЭn De/c~UpמvR]y'ݢW-MZTP*H`&-J׿VE&joq·w6ek+nֲ~~rж=./1sIڤ1"^VvBIY-`[n?pfxpu/$E-EԨ3f^FXOa+N}Mfp򌙛Eڎ 4i&,eƂsF' ~T(=1 W1b45gi"S']>|]-{|fF P#p ǰ/W͞"g@t Rߐ-/.Rϩ}#on}r*fym.o=V3rP *mv~^Oɜ|/{NikypbOWS'wȎ2%, :h} =Ŝptb犪3E|RnXM59ž"@}=wJk;kJ}~H{+'z>9 ݵc&}&M'z:+:(u\B9sf;)iKD+i4MSf\x~PYyM K N\.ĶK}*yhW)l,ghAiUU#tuYt;k*)i7 n޼YT8̐c]!%!.v+66^W(L Ԗԙ"T5`Tm$}"oxJamz,luS0$C^QiBV&0<yph\g9~gۍM+B民J=ED/X0P:Y}[ʫzmQZIj%l֓!Xzy׎^b: X`tA00F@2B& [ގ%k#C $ږ> W\acƌ#,~<1ǫL׏?U? ?q\ 'C'Zm Kikt2 =i.;ծm B,/KޏzNHuz@OP6|HPw`$(aXҦ '3 _`kːM#5:4Zj[LIXJw^sy\`2d V@ D##L${5e؞X"0`BXLa3ߙjW$)"UJQcNꏼE"/>E1_ګ0 B)[ՠ~9.vęS\DaݗGf寶^I&ې279r[-JH%h|BJT,k6:3 ȟAufp!+`jnMFx$\P; #Ƞ3q\qiZ NkCh2M 3  ~Cv @čx.p(t濙Ѯ8ht.^q f?-x奌c|E<n! qIISC.1+彘~dߟ#yyqA"<H֮'D#%RШQSaSn#R5%J8b-q<HFUlIXʼnD4P6imO \;;[" GpU]-܀V{M&FU-qAu\P I=IƽM96YvZ ƵGw&D-W=p}زxv‚<0w6(0u"YUB@|$Hqrnٯ}i3 *.@(U+u~e}9t=eFH~D^j5+-D NI|6v7_HbIKSI,hr\xKו @*Š%tOml,l4ܗsKOKZ#T@HYpxҷ l`d?R-gxqś[Ӎ:e ?!-=S&(mGSm4tHY[R;˚7o,lj(8[?͞=Vǚ۹G<uyQ vb7==6芜 UT1H$;w2X?|?WFV !Y=Z72謕{ϴ G/rۘX0UX嶽B ֎|qrB{ s_ChSX:L8࠵8h/,*SIENDB`termimad-0.29.4/doc/dysk-nocolor-ascii.png000064400000000000000000002154631046102023000165210ustar 00000000000000PNG  IHDR;| sBIT|d IDATxy|Usl b KF *-i]V9_tKί2uA8 "!,!@dsGȅKsc?ǃ9sysd>AA&(@AjɎ  HvAAD# „{/w 7f]_w =;  Lh"AaBuPXj1Է9hhw\.w\}ͷ;V⩧bٲe_>+sύzA&:eTCiVLQ̹~b|W]A|Z6QmB! ӳY:=]߹ʓ8]&3gxQ{}Ԭ[YowhB?Eюj{^2uV3VͽeFr 49\Mvz! LJ}*&ᦹ=o7B#cQ=|Fg,Ն̭aщ'5,ЅFP}r{6;#&ߣPon᳿+isȽǨu5 ^Jvz%3V[<ÙejMAA'D%LEw/8A]̹c,+FKRdvNy2zؿWGEg^'w@p UbmmsW#mS[JN?O6xkI K,H7tɑvJېd"8yR3]nBJrf2jwkxhU,)їg-OF.Lwl;ʶíY09kűJn/t[C]w70-Vǹf;as`~z3;=fy:?5RS`4w:u2XƌdCb$ 5k֐磰0`^?9o%%%gfݺu׿ %Kh"L&===TWWy!ã>Jqq1|ɨA*PWr-h/rEzJ斺3F껿 2>udi止i)Gg 'UL7DGVz k{t_6u2z;ar9QeíqKSbttzR؂R)cPBuJC8u+ )9ŌdÀDgxBr2[ΑIˎO@Nu9$ 9luyY6; -<"<7w7dF2@Q}̓nRceF(nJδ8}!E^^YYYX,N/&ԩS$;sDgڴiql޼ Cn7>>Gy}{A*{zv4z.gv!P]Ajk{t~bkk 2]VT1)&ʋwbuz:K[=Π:tart[8sƊK9_Pz_K[k{_ZCS3p>(8e4Z"`2 MLiT]ւN#;50)1:fq/3kӵ]x/.krQ`v+ RW"+CFNFR`LZZ}ɡh$--bH<eeeX,ٵk=))G}]vDGAe^&Zk8mq+?%f x+4:n;`ndѷJ2k/>L&gRbKޖ^iX v]cTCX{8&Sw6p 9]= =Peݘm.ث!&BfQb^py鴹(>kXdquxFmT5ؙ.*0*)/ild22@qI.ŕ<ҳ )qz)8I]Kχer?}!VEfٰl媪dΜ9ݻ9s`Z)//)--eҥԴ:XƔ-:%Zv[}!EOO6Kɲ`h/9Bdd$K,aҤI9rd@CEE~) !!!zm6sϐ= B9@|4 RBoOiVvTKj2/vR!4*.RO_g{}gJkm9eG.:Fhm|^:.tޞn:.`m>kWl'X%cgPq9{6w_*|!o٣ J&SytzT-^uߠn[.[-+&425Oěqs(~ARf. *;^:snf2 n6МPT N[S҉Z & ;˸9Qωj͍Uoޱ_cYDEEQ__O{{e*++Q @8nRΫ7"u͓$$$`֭8p^/K~zۛ3g˖-#""LQQ|>%Kh"L&===TWWyf,?y뭷())/3{l֭[ǯkF\OHHկ瓟-[(..sRR<夦r1HHH`޽{m5R% ׭[\.vB`߾}|۔OGP(Xf YYY|> zã>Jqq1|.L&_~RYYۃg6>͵lFy'dZo2R>V3Ҿ0}۶mY{.]ʢE0tttgq |)]w݅ZfL&;v]SF";RkY]dW^`^y駙5k<ڵkٺu+DFF~z\.`ڴiql޼ :u ܹs9}?i=ÿ}Yv!ؘ5 ;wꫯxذa{칦'd)چwywӧOGǎɓ322{9w\PBʹ7;;իWs9hoov̬Yظq#)))FN:0Rjd…RUUE[[2,zߓxGٵkU끓6oƪ}nd۷cٰ_@}gΙ^˅"<w6hmm joo>#jzl6[P.ZX))m83ތEΟ??t)us?el "xi:>e(׺}nTcR>3~2{w>5F:^-)P宖νù^`*NJ5lZ=`nbV1"ЗWTT駟+BJJJ@#Gɒ%K4iҠ?=LP' ccczbccimmƮ}zzzlH Fq#R륤m۶p8{NL{zzTA 6 }Ƌk>uH=g\r%x<5:::CX!'XjsÝ{AZIvK17n7Ft:݀2$&&hDܹ tRc…vm2fb`2ݜ۱ZqSQQb /u=]`@RdRb܆vZ;w.&)**"77T"##=䷍`;bL6%KsMM qqq̙3hoH# iZD%c>|v tM,_8j5jWOff&#**UVprcs#{ԫkWq}-[}իW̋/PԩSvBCxطoaв~l߾oj%\ZZʡC7bϞ=L2R҆xq-gRj`>;}MB$z~-#G0Z H{{;.\rCʹw$#{ԫkW?Yzzz׵hZZZFn…^7N'c,pݺuj^{/݈dzYʍ"j{%)k\p-[FQQW2D ׋Hv${IOOlTρD ׏Hv$Q)mo_HA}νX  _HvAAD# „&AA&4 0dGA M$;  Lh"AaBɎ  ڠ/AAQE_ ݈o1_7b7E&FAaBɎ  HvAAD# „&AA&4 0dGA m L4ovUWaU^ WAaBɎ  UOvz)-[v7#L $&&^0A bcvr9O>$Ge׮]cӄg(c儅̶mۨ}3grsQl2u$%%zjd444׿1 6cjkk-)++cӦMAmCT|r1Lvjkk1͒r{9<gϲk.l6*/тF5mv^80oY\(ߘNFI}w/+(Ͽ;5oNX[SI59w*کJ?6lk2aeL&54s69JkKr _X#Djdva`l @h\(SQjvVFe6LKpBL!I{E; lg ~"X ;"Ak 4WlJ{:z[A򦶒hrhi$!cҍrDNccϨx]wŧ~ӧYhwy饗hkk z]֭:X{1Μ9on'667u^+^Q-ffΜG}DSS4F?ّR`Ϟ=ŭO&gڿŋcƍx^Iu]jl6YYYd2رcm%$$#P^^Njj*ǎ#**ݻ%ի.\|M6΂ %TW9 IDAT<:3Rݽ^/o&?7oq(J}O?4>,;vەJIIɨL¡Cp%"̕v;.\`W%ٹwZM^[MvBBQHj*'x?.R&[m]`Eh$4uLbn[Rʼ]?dLfMJ8u]t r,12w_drr}RN'2!,4F H2 Չ(4ѱOܒɱFnl2*V4Jڀy <ɌI"䣲cpa zr6{zz?H .*x.~$//1TUUo>ϟ/sqfϞ\^xx8?~wFӍI}{MɟK[ R/s+3IbxmE*H?rSymI)pܱzo 3Rfp{/(: o4 L]>Ih*in^FrwGRvQ^<~T cC&"yl\nzd7^ө<ˊ2]~5ͼSOE?ުJcD+EthL!}1ikqŮeˊ2xɑ;t̙3`߾}ӵZ-zK%m6[@͐"##YxqPP /k2}tΞ=KVV^_GGGßHݖ{z<?^R)^II12#,UkkkIMM}I޽{hlld̛7'OJچ} pyoc0{x뭷tquu5?jj1ś0yl\n~b':9 P46onPRk9zipެJc=)̍0{uTtƟ+VpA?LYYH˗s8O@d2Ix<8F#MMM8q+/.^ZZZttt`1>/&%%eTCh4aÆ߸q#ovSUUEUU}?0+WW_ ̕BCCZGwJAI8_ٔ?@ÍŤQR`Pkзy\Z{\D_e7Iwzu]XVѕےRfEv֚)6O$\x[7+k;_m>?v܃ڠ&<%sWu{Qj8ؚlcCtFEZdtwi|h)q2avھX͎(>T:摮;m (((0F\ܥ`X,bcc>wqf̘ABB111D )zԐ孷B.8A#ETzH]]QQQ˄ufqoyFnt ._~s/82zdG`*:XϋjNΚ΋J~*Se2RC5T[CǬUpSdIȷu嶤9l㏥T]ÏgfE?.G}gR[[ %%soI-}I}LL ={>{Lsy1Oqі.V'aP)>O̊Xk7=.BJLyåd6ΎgNfwBDʶF* mk\<~;v >GtKPe5f㈘AWKaIa(T ,?mmg3y>QPɽ̍p6l@en5*I6R#yt̽(k5ѣ ܎G.^Cu5:эpmINvӉ^̮]t:Z.{P_iGvoMxd28xo#e[R\O[x$#0Sv>W%<%VpreI2v;-[NxJ8ݽ4o?cDN$>;d}=BAYV4 &8= nMi'\P?c9p7O)t*7.s:x8KJ:D:A%lCpmEGG-> |,ጷxƛu֡VC9Ա1!4bםI1&gGRQQW 0΍MRΝ;v,xAM\wnl~70AAD#W?_Afo6AAD# „voc=S>|xI&NǽԩSQlݺ^O =܃^gӦMl|(׿uy晫TNvr9O>$Ge׮]cӄg(c儅̶mۨݛwgΜѣGٲe˨cHh"x饗q#ٰa;v[T|r1Lvjkk\.}fX8{,v xkdU ]3gd#Ỏ̚6;/{㲸PɈT;7M.Jn &_燳bhӃ?c2aeL&54s69J gDc{ꠣK@ nO\z `)&-ispKh\(;4\<6d2))( j_-1X ;"Ak 4|EFu9~s,ҎI㦥['Tu3 ʭĠvN:'@N0/sxcc#G zWӨLt:xFm3t.>SN>͢EwK/SYn***zۯffΜG}DSS4F?ّR`Ϟ=ŭO0S[TK׉ XF07K=4ύ/>3?! R ʕ+CVBAA'O_ʿ|~ٳٽ{w1pJ)Emm-={gy%KZ)l 0;ȍ pm@#vV"M2,5NhS"'Bdz&fyCGUϐL&%5J:7sڜL]6}s}NImXR^KNpM%MuEO f+X#;16Z8kZYn';7ͪc{dr-j-Z /D~vHdzdl/G}Hf}#Ȋp1eSFk瓝?8,]Ea4>ʌ|8\#]z=/e~i -ym*ٙ={6&i@2GVV:;32999][RQQAdd$ׯrq!, Ν#333♙ɓ'$:0k,>6nHJJ =;wN4 ;wꫯxذa{X/$O||$;S+֣̏S)7̻wZM^[Mv7x4c2Yc A';N75Peq Tv!c2kR©ϧ[`Q|Vg';W1tLX2p-Z Bp;X,XښlL{.RGñ˩>fF[(=\0kӭN%9L%dd'Pm8qF#Q)Y$'L|+uaLfӱ/ wݙ6mw{XV?@vv6Wܹsdggs}5RќYK9~YfӜW]п 8@OOpB {7K(((LUUc2Ǐg/8<gϒJ_GGGßHݖ{)<?^R)^IIM(5fZ[[NjjjI0k8Z< CTjijjy UWWϓԩS3gVbӦM4)ere-^)ETpKGn Tϒ&.P;5\qZ*嬈%%4Vv^~Swg}FOgOYA^ޮ^t:FlW rj""5b-`8 P46onPRkю01֫`N6Xk+Hםso}}}7**G,S__O\\\Pe"f]Ί+8x ݃c})++r9AYYYYYP_&`5L&^Áh'N2bj]l{Ql߾V^/=UvSUUEUU}?0+WW_ ̕BCCZCοڼ>ug{H0FJ8/l$ѠƤVdbnK\Xz N;BT׬[`b)f>`fJ (9Z&,)0$2SiUh#4jy|\vڈɇނaG5c殾 f-'KSy㤴dp+0j<4wi)[^bs#&z=r*;􄅌}#]wFKʹׇbz]eddEAAy===l`0tv, ^]]];~83f !!F'R5!%%[o\.'11=!66Rwk.F˶L&#%%?J, vI& vP <磳qez=ɔYl_ c8*:XϋjNΚ+ƾXz=|Rczme<;7+qyrsU)D]q݀rD^W~N0MՕ<5PFIF`ZBH82H:0AUr&o_\wbbbrq jIJ~#9TNggU>_TN^^EEEC6@QQ?̝w9 3ܹs'X,Μ9R$990 wM]]ݠJʶF"^lذ+VPZZŗZ IDATjAL<;t:x<THHk׮D222x׃^_ }>---̝;d2+W-%uSy( AuGqwRXXHbb"gϧ?8>>[UHÑbz]Mnn.x<(INvӉ^̮]UW:_%aa(T bb[,^6kD uUnZZ/{wU.[2VBH$#SMa`lӹ?v۷\NtmD.1h2ϕ~pRRd yZֻ^C͘1xzIu^5MֿIfގ5x:w7iK-HNO^{t0e\-6@^+/pb ٢i]S:\p^OJJ٩ӧOjYt)Zvɵk,*ӭJ577#|cQl#¶yg4|󧞋駟wB!Ĉ Ɏ{aW#B#B!MdG!6M!B4IvBa$B!MdG!6M!B4IvBaz}B!ha@GiB!#Ba$B!MdG!6M!B4IvBa$B!MdG!6~SFNpxA!ȑ!B4IvBa$f/k e:ktN\nwۋ]k^{ }@wbƍVw=lڴM6 / w8Bdz6VٯZ ?/a}fh; 1Dn @bb"cǎh<> bѓ?+R8,{ Qv745F=3~bzQ<,ߒzٯe7ҮpZFZ<$C!=%y Q]=e)8sY'싹_̄)=ܓ.b1*++Me,X@ll,ZZ+Ο?oq[JyWӓ(4 |GYr%&"##QTdffbqv,KCLvk˒zKךil1]'jOͿwWRTSeV|aOULq۫3w{;Sld_mfj[Dx4jS2Gx5ٽwǬ9uDLrUjh5}i` OԻ~gVᓣ̛:hǛOp]i7z]Jij"י8QTJr Ԅ(^Jɶmhhh`xz~w̒ٳgs%Sʣ>)))͍>_$--GZ5xyy/se?Ehh(dg̙$$${n9s&WRmYRϢE8wz>jٳg6l 88דGqqU%jAOGW:hm]sC_*/ƚJBg/<ǂiq?~̢$&iomЇoXۣeI=wټq%Tֵ5!5.LÙFSݸTڌ῾[j3ݸS($nv,G5S&CXGjh-izLp#1{9f{;RXfCɺP"&&t ?G2O8O:EXXCVKXXYYY2^^^zrssihh#GzO'LK/đ#G-;wRQQAUU'NfeΟ?OVV>|2Νkq[Jihh455jd*UWWGff&"jjj nZB -Jn5n^ϣy3qz;98p^?}/IzaD;_[_m]QZ"4f~(Nb0?0֡g=7'{ߝuk$Jg K&>Oj [Bf<{xPO FRȓZ vUI^y]Ԝ:WhhBo4k7pG %quu^SSMMMf c֬Y;vYfH^^ŋY`:q…uT*^x4YHHHb;w4QRߟ Cmy{{s(fKzEu|K:x^mg]sc<-Ggt-8ELsM 3oϬc\iWʪD&`o÷&E3ƚ]Sǜ0\{?{X:ӛAe{N5\ȹd5zTGIэ-״si=⹝FڒxVݝ]}1%U:ba3.8ki0S.hkklo 8}4^^^̛7qqez=:t'''Ӆ 8pNǪU,N:yf' Jʔ1a„~ۯ% =jjjzmooGu=Ca# <ܸz,L؜%f:)ԜmMOk$Dq\(jN Fh%BL ˋ'x׽FrssYl44_7}tΝ<Э+**zm۷ʼy, jZ ???y. ""9sҥK=+--%00Z-g(x IMvnO:nNj&{%JօGՕ{^OZZnnRbʔ)=[^͛ҥK?>ۮ,[",nTWW?Gy=_|ӧj,]VKMM ;wڵk=ĉ<38;;ʁ,g(xS=v9O=kJ>< Hhs/zCBB6l !E@:0+td<`}舧' .$33S!bXƎ#WFZ;k,.\رc'33tF̼y󈍍Ã6ؾ})_رlwf̘ARRo6[oe~bb"$''( &/GHHgϞۛ2228z蠗`(eZHKKo1kS:+V 22Hzzz2Ja7>>>Kdee_*yʕxxx׿OS 8xE ,f(Hd ꫯ'QQQh4裏LeZguo0**'| 55j1[]`hZjkkꫯ8s%cuʕh4DRIJJ `ٜ2ށd} en75kzZZZxי>}:k׮t[QQQ<ݻ|Xf z=֭믿 ~)]pk#_k.|||x嗩ҥKD||T22;2USScT|쌫+,ZWݔwJ /F1kG2i|FW[sXcyg>,.\ 33Ӣ~)d͙3g̾WVV}5{m^62gF+'77Whss3&Mbĉİxb}]hDEEq1fϞͩS,Z1g}+Y@IzWT$''իMx,ǒe8ROP/J[sXcMRSOƞ={,5 -%crJ ׶R#zW~h4ﷶЀi׈Ltt4 ,`?l*3}tΝÜf466˲eϧsÅḹ`Q+y_02[qƏٳ2eٯI|233#$$///x>6wNoNhh(ͳ(撒5kǏ0)G2dC|l[kXϢEth44ͰmW322`Μ9x{{tRͶcJƪ%*1жWI)_FifyGHHHƍferrr8q< ΤrdKHH >>;w3{oĉ,Y@F#ׯ_/pNI[jZϩSٳZMUU\p2le5T/_o☇r#ZZZa*b ٺu+111X`F#'OYz5---DEI%ZccfX~Y"$f|0[T*0;`ggNJ+h4g͚… ;v,dffhÃ_WEEE̝;x6lc ʕ+h4455J"33Em/GHHgϞۛ2228z~u{5RSSygٺu+DGG~E1wǽvZ8>2NJJBVEHHvvv7ߘ,Z>{jO?u韨dΝ899[oKLL$11dLj֬YT3fѣ?ޝ{JRJKKr oСC?]蠢vVXAyy2r*c00&* www8{k׮ŤILcVI%Zc=oz=֭믿h4/u(##ppp1444?>~|ڵ ^~etpko<ȉ'8uTuf >~3gxGgt:Ow|MRRRLN<UUUdgg:ikkcĉ>TӞz}}=iiikkk ???{nnnhZV5lV߹s:u*]$$K(i?JURRBpp0;vV _{{{UUUׯ_;MԖG}'''?yQWW];Ö-[LJKK)..f˖-w`FL[ՕҾf9;;***5kU[[J\p;w3ϰ~6n܈=VT*4Ee?blŋ_z<[kCˢ#;dffĺeffիWg=>Lbb" \|{{{jftb0x'~$45%JOOgݺu,^/2w\->¡VMpqq^ovɉT2e ۶m3}ڵk;w.yyyssN}ڴiDGG? >sVZN}­o\pvz=G:;;@^z%\Bii):`fϞm: J}||?~<񴴴XE}W2֬YåK͍ `08}yxꩧD3qD:*ik2]Յ`31IJJ"??7oi 52JƏ~͙3Ǐ%1x;bh)NvGՕ{^OZZnnnfedHRNCCEqٚfNjJv***̛7NGNNExG 56U%GvZ1߮yn}%&&2sLo̶ , 66VKmm-_}UGWf0bhR wuu巿H㠒3f#'22m۶4+?޽{ˋ5kɓ'ihh3"" .>}:{aÆ ~z(..Vܖ#&;;5kYn_5Fq~fhS|?SZZo_|]_K.YTO.>۷ݝ|:DKK 'O&))vrsst?7|ڭ'r>O))sV]ɓ%ٹ3fɉW}#PUUEvvvܚ|IN[3o<ϟϟ>/ ",,iN:::y$Ycl7Vdjaygl;?yy(m2e ޤ&Pbkk+ tknn6ܹsL:|||z\5P~:fyzzߋqXUʒ5bV2%Jth{_̖Ju"tppǓ$..WR__{d&11._=AAAhZ :sss1 <\~IzxR@+==uֱxb.^ܹsqvv"[Z}L...xIMM%00)Sm6Qnh̞͛=rT*K,h4ܣ|UU\pvzE^z+W. ff7TRn%>>>?xZZZ%⽖D~~>7oiӦj7IggguIWW\ %cl2]Յ`z;ZΝ;h4Md3gǏ%##k2w\gΝVy ƏJ)w\\\X|92c >3$&&RTTd::%e =(fR[ deeVS섇-9+=z4deeɂ x蠲9N.^L) IDATHTTT멭e׮]v/%cqgOj)((r$Ņ'O{nYfh쵞K\\˖-}Y|V %cUذvU ΅ ppp?}4ZKjaΝ\v͢2*Y鼬Tss3<#$$$p }Ae-pC/6m ٳgȂHgIJJBXk}Lb!; }!_ Γ"=Ir*c;~ϲ7^,eg{_}AS-nc?s'j;e Γ]9ot/v\>8F~~vMy0걖O&83y#PAAw]Fb{K&@|,z5Sp^ Os3љ Sz&'r]b,.vU~G5~.ׅDFFm6X|9ߍrrr2KvfϞͥKLNhh(>(۷o777{ߟ_|4=jqN~~>ɓ'D{{;~yyy/se?Ehh(d_ӧv^QRFdo0еAg{M8S/|KcM% f/֤btNۃFmpwPYsv䔅,X޽{y0xwя~gsAy"""X,FOΟ'MLL n2N'fLKKcƍ|'Fƒ@II cԩ>gGNllٲ@SS$B |)P5$ t'pXQ0fzkQ{<&i 7Kߦ\Rp/}DMQn2[pfvV~a4`i"ޤfrB8Q)%kWO'|Vߚ“m2Dx#;mٱLOw$Ӓ9yƴpN\1+4CǧK=/x;;rt<CL5 UE;~ˡC7o{aܹ]2nGvs Ƒ=\\?}aM45%1&~/)ߛk'ʂ^VwlqpQFAwӍlx. Zߟ^̜lږ.ۻ1JO_JO;=$L ,̎drp:koKNYȡJRR`h_"&&ŋ7n۷W^Ax8q۷opp},l696$ιs0vP(USSCZZttuZ=:96˅ k?p4' ߣNoYgrvQytP ]4|-{}'ias$2g/x/V /ai?l?T\'qCn=6W´[Ze+L-a WS7̅'ۙ;8#0wgڹPoÃM\۾/šr ŧi8o`O<=El;HbtkTS^c'$DiP*/3ctcHę?o>) 9Gff&111s=X,ʸ;l69s&-"!!m,0^oԩSYxq¯Kcc#s%668>^/:Q@9***h4r}Dll,z kkkq\a4 @ rH|q=L,[= ~(ԹC:Ԣaoe҄~w<}6.~[Z*+%"&goֻ 8]CqNC{7 F_FK/5RVVX#`2¯ @ @0Ύ@  pv@ LhIj-bdžPb> ͂Fny9;JQ%]BncC(jEB1fh#4@  pv@ Lh\PC@\. [,`6ZvX7fy"لb> fmW_nɫT*/DkkvzQ((zfHD YhH6Bm]x6&,4O$PgY،X#`B#6@0#;@ &4@0Ύ@  pv@ Lh#`B#@ @ (cccZ&66>av;UUU~6yyy<޽?0<V"==G`dbh)S7[>,j/a޼y} ?Ν;}Ʈ]8qׯ^vqF>1mKnBO#x 8}4 /䐟Onn.of~Õ$''裏RTT4*=rPN"ʟv˗sQ~_v`叜s9rǏ~gggss…%9mٳYz5.\`<<3sL}6mDFF?8\pAv"^9wezocSXXHii)TVVRTTm泉vSVVlߢ2}t"""0L>ÔN4466@MM h4z}qr}zɓ,\w^n ǂ ػw/477"IR xؽ{#'?`O(c6ٱcV"'ʟ`Ӊ$**{m۶9,޼<;Fii)ڵ-Zp\mmmx8<---k{C5]ܰ5;V%&&EUhRBq;uK.瞣?jTVVƜ9sسgsbP^^P8xzx<nJ5>TN455L}kEnK֭[-szJe@z>e0:B1q9ifE$~:;;yNW0:h1T;7 qG?Fwnc6ILL=]#=yEE1 Z ?Cŋ:9\+v #9y7:HSSj= FC9x8q۷opp}vvv= GN?ㅱʟ;䶙݃+W$557xpIkkbqY 9uU.õryEN-`qrh4ϦT2331ڵtRIJJb~>3gh"0Laκ:x, eeeyTTT`6 z 0 j?ZPz׬YC|| fmW_Ě@ @  @0Ύ@  pv@ Lh#`B#@ @ gG Q#IR"xgYlYFn Fjj*XjB`0B1fh#T#@P3pav=`x3r ˗/'22oιsF֌3׿ÇٺuHKKcդ!IoCƍٹs'UUUC~zؼys@qT*/_ٳ1LvhooeP(xXf3gϞeX֑%^&'yr$5űZs˒"QDfNvߛ&G]cv ~Ǿ㟦Dik[*Z8o_75Mddz'4锭ˀ1ш)Մ&\8;_j`  ")QTtwt\Lg5% I ^vRт#,foK`vRۈ F!ASb8`ٯlb0l*p߱N4jrJZ|\GE+[J#g'''pgČ7=ڵk9}4 ./2n:n$&&s|Mv;6PDGGzjGtw͌3`00}tFّcgq)bccYd O>$AIܛM߹X=fdz|+G;=3IeWn~{j޹)hNKv%ݨ ّp6r_ceJui2`mxfNtco˶ώ|+L$&qeW.ZGdVvТ1jrv~s Va*N_udHŮƋČx+ͬLMal9?OUj4:$G8H0: ~GÈIϧo@Trwe޽^?g-[Ftt4픔w^^/&ꫜ?w͢EgӦM6Lڵkh4XVrss$v +}Qȑ#ƒBqq1~t{% yټy3YYY̟?>H^6l`zuP(\.233Q*l/_˗y뭷|džMҮP(ZZZ_|ߦ-[jO nJiiB`رHYO9vص#lrll6UUUTUUqY~xb>i OL/ZXjw~u ӳű:rbYbu^6L墵ߜhȹpq g=3T}0]Co8ѱ8D$E96TK$JX1RL4_?cvЧntnimtl;Vgs]ǜ$3~Ύ- /wNo֪eˉd߱[X2X#ٙ5k&3Onn.of(?yfmFEE111_Ɂ0\p?$''Ǐ`zfΜϦM .Ȏ+,,]vq ֯_kfƍ|gxa=SF$''3}tj5UUU `6OUUՠNo:>OS)ɒwWLPjѮRx@ڞS4_1\#̘1ht:z=WKZVAâE|ǕJ% þ ֬Yôi8{, V"'.ǃn^J%+]IMr4˥/얖233}ڠiڳguuuL6[nE#LΜ9Cii)wy'^m wu?_|tLœ9sXj7o9rl#orDhG%lf$ˈ;;+V`ttt x~ŭG}DYJ99 G @Idvp80s1kkFtMccMkk! $ʴ6-- EFFƠThdƍߴi?墲J>~aV\k͵DDD`TF3|Ǯ<|9?DÅz+.a*,݃6؜n:_yjorLz/Pmu1%fEi&-R;#t[*Qa*6w&=3yc<^>꓏rlz}kg:;4DeDQg1P6^˃*LE ku8 jm^uNr2y5uX›S;uhmwԣ+gRXp`Mll,ję3g'qzݎl&11o@=z{wx7r |2x<y MBbb"* tuXk)ӯ~hZ~Op뭷>rFr%~_[v-. K[[7rmz=X@T;xvc<^65v9gvȞW{$FImp*:5= we[|V\ڸhHwg%*<ϪT/ 4! 96#EBMGɵqh#{^'o8ـ$IEaR\].F,ATqQȐDЪD\n0f'9T-PƖ|JJJn8KJJŋs]w{ߵkfΜ9J"==跠 ýKuu_'-W\5rҵw^6nȊ+8u-B<¡P(||j5$&&vFjZ-k֬Ty7|狋ٰa- 򕯰e˖}ƌ̟?r}Q]]7 =oYYY?~.n7o$tr{gRUU ##s@S@||,<93#M4u:Ш+=W;ʛynv22cVqy!+R+{}L q gz{J!CN #(6cͤ'aoݳ8N.ZGƀl/4;)mDET+1?$NfҌI=ǽ8H#@07́~{s-Z4uhP+dYɌ >gRdSA&/^ulvjΨڄ۫ #rdV#[d;;YYY$&&j{nz==n" oҥK9NN:ży\,G\KruYnJ~~>+V͛7FO?7LSS/|ˑ$|;tuusNߨɓ'ٶmyyyy睴jİvZvsl MyW)((>j߫QT駟92rl%K`X(++cݣ8bӍŚ(j:ZobP+r{9iSiJ NF/y4;Hv~m@/(:5^EF,}J96;ETF}c5L!yv2H=#Bu D aJ7t,h!JhshӉ&J7vt+yuUϟ?o̮ ZE@J0"eeej5QQQ|[ߢEx3Xn&h7Puc"4 9 |hE3[dt:***h1`b#Fɮ]F[lƛ@ LlD(ccc㏓'O-PBCPgY0)/5;@  yl @ (@0[V 7B}Yj5/^ Z'Xu#<< 6p=pw`emjz-L!<ye|}q뭷AN{,yyy<޽{-EP3p1IP[n˗ICC۷oܹs# kƌ|_lݺuĚ.\Hll,/288n86nΝ;G(A˗/g٘L& FP /={fΞ=ݻ}K4*DBNR;3'gNX_㲤͎d8sL!IIyjfEuV^;=׮ Sx$2#\:&IF ={H9,ZϷhԕAݱԚDgF3@.kGkcN>… 7/WXnnpKMM ---c`}̘1?z ӧOh496}N"66%KO_࿷B en HLGSu_v 3]q{ӇlB;E`u-v5^$f[of vg>7wUc;(Ȯg84X?V};]Dt݁3ar~ZW6ܹs#~-FHD~~>8WJ%w}7xA3g˖-#::vJJJػw/^~^}UΟ?fѢEi&<rӳvZ4 V\$I;wȊ+%%G}r2339rP\\̧~*+]^tB|A6oLVV磏>W ؾ};zyn: .LJ%EEE|3r멧"%.999l۶}ʕ+IJJBHaazVOS_ĬYO9l6ٳ?d~{{Gp*%EpSQH={R\ +SM |tʼnF,|mrt@zT|w$*ofyrĈl<^\$I>;tI@T _ŔeSt_FEqy5^uv&G0;?Q5kFj$$, F.l Zg!:qο/똓dfrDx`f5;'1/ymAReֱR4gUƱTظ%彻Kl0hb4fElwgٳgr^KpBF#|'I{8/9m^'?瞓cÈYfa293o`6뮻7ok֬a۶mTTTq:8p̅ 3^HKKdOZʯ l6!>O633&vtJcPgg8kٓ;#j[7nD!#s|Rm,IAϏdznP)ń7*a2֤\mùʤ잍@c©=Rˌx a*7u.J^_-CYqD=_\ՙI\4d8-](%wJ4'[;q?hȡ.Vx@ IDATh-91|{F`n8뀢3覣c1~#R$2=A͑uV0N]. zJ4xFX7n}E/?Y l9LEϦVOj<825ζN{b(JpF,rۈΌ7ߖLFps0;TTu_ f^/X؝=xM~4eߩk{kjjX>wMMM IIIC~9 Yb泥c'v͸>As8<%=5$Jp`4رcjVꀯ7{yh4ر&<=Шv\TVVRYY'|?ʕ+y1yt0<^/WgwbQtTF36+ٻ9?֞3]n'ZicH11ͤeWU;]jgrDnK%*L\Dy2ګt4\}&""Lp#N.FG^[܋D0ט=[GX7<^ht_jobiF o猸=c[_GRsqˡnZDjG>7\3R#,_urvCsXV8s onc6ILLv1ѣG{III!!!7|3 = '˗/wAP=zILLDRr|)כ$;[T*. c6R^/mmm$''_^'== i^$$®x4Vc<^65vM'kvH4Xg$绳ytώgU헆35tY}gOnC86rLĔbRMx {qXh5~T:ՀΗM{D@2kIדI($HdCd[}E>r$I9 III~Xkkk~^kH,ojzğ 69;`IJJJŋs]w wEAAf3gΠRHOOh4-,++pR]]=`G.r9ڻw/7ndŊ:uE^dP(4iSILLvPZ5kPXXHjj*ټ#CKcc#sΥIXr%^SeSSYYY?~.nٳ~̝;={d=N}BB1蛈ͿpqANΞ=֭[gŊ444y怿hx}ijjxyy9$|.vu $9#y x駱l߿*-[vZeee䐟JO?ϑc|r,Ybݻw߰(tj/nLi?f^nweIM_~GԨ8j~KDdj$JDj3M}HJUt4vTDdz$1Scpv:i.or+YBΉ#ߧ9R{@͓/~džwgCPޢ ,²f;4~4(Q+n;wM:tȪU0e.]M/Cr4bغu+wqW^ziDa ]j5QQQ|[ߢEx3Xn&( ]7+BCP牦9Pr똬NGEE a&P|򕹜==;"bOήN>y}ӈS\2Iw3 u䢀5&5JLҘ4mۦiڧ/۟MOsڦMhxI|1EE4BE*reaf?(=8 ~{Z;k}k%dž^)'AhsdrfůD5d297.#;M\U͸_HTAg#NÞ7=1υ~.w[`RVVFYY.g?7h%=W^4 'y`3~!&~ g;gR~}^hvA4Ko0]dIl㔞?1lkƼ&O "+]62Rlld鉳O uzoҵ2 < *;)jE݇pDwƁ<ʟi箶YL A.Yգճcȹk`MTb<$7XE[:ƥ~lὓ5Ryy3;!$$+WRL L줔+ TBCCy&| uuu@=߾9m63=RlkXt)IIIT*/Ϸ9/)M\\JR}][RIGGd2rss9vzBT]ƽ RiFm7i{ | ',zřkjhubXClEcjH$WnkZj >aVky zYz5]L~mPt]M{wUʥk<<9vVN|z2ϕ^2Q(dUA4g +&Υkjq7 t㬔Ȩ$%ʇYfZcRf5Aer2Y6ϛ٭ndd^|p{!\x0.P?źNGWa.DMɁ.2o('zL1pytwa0Q(I^#3[k6y%\iŁLenq=y;m ,leA'.JsHd_/ Sy&6E-$̬x)HŅvcp1w;'X`*rl`Y#)zps`EfOw79Dmg8tit9Kf-ŊvkBbb"ٔ|Ǽ&6Ν駟ÃvT*o3(..FFuuz= cSQDJ'//Ǐs}h">CN[[G5{;J\***hjj"88ث\P`5d2pqW,@ɹqq=8:tosdZ}x(Kl.py(]Yots93>KiM<{[u/wәNZ;$g:ݫoʷ˷4Z=-ZuYx_[?ݝyt1+ЕkjU/0J`ԡd2d?frpG"ZFWOfa 75u3xZ[H777jjj:::0+//N:ELL 픔mXt)۶mj Ғd<(J|m!55b3>fbBJJ ӧO B#) ƍƀncR.___.\`r4ےεklJ&k4FG  rJgW46:' G'48n,~dN.yMEbŝ"ɍȦGV_EaAmzQMuC< ?W29?,7dM@fa :Ya9W%"4pY)gV+ tu5BMg3zG%`GJ:~UGoU}pD.Op[>õm`0py8u;w:Zk̙31cX_$0ddddƦMعs'6ŋfmy'P*=zz=O>Mr uSuWIԝg&F^lEN,z z 7$s< 2G-ܸkU@&\U#{=ۖ Jʦ~sIyn v7;ilגdaY)YAmKXgo\x)ڃ4X;Ý⛝4P˜Pt8=qpכwB'N} pACl/fE@SGїBww7&wTmqy|||Xx1SNjN3y7qvv6.l@s%9FaÆ 6jz5LFhh(_}eeeԩSM^k媮ffC)f8Q?RLPPɱ@,KTq:cx#"ík)<@k}>+j"Wi{(xːˇ/ڥorNd_n1]:^z;iN{py(Ȥ[.sSWQH$6N2T tr;Vbg bg8sʺ.;\KzsV۵ԵpߘNG!5=)SOo`p#U=V Lq:z!2LtBMSv-ԃ#-Krr2aaa#X{mooիWSZZJ[[sh"djٳp/^lf41 /SNeÆ  \\R,T*6l ___,X`qvMM }}}$''RpuuX \999DEE/V"00ӧOWUUEpp0aaaT*FX0 (e+yO@/TSL}ؔanѰO˅cՁBĴx4GO7RU?k~伔ήeFҀS!13zM6R(Vk/3͒Sa-d2D1}(&G/ Ă{Mggr_&K3k &KcO\nTN\l˂fz*/c=9Mܝ$ljERB 'N͍'|NGVV)1{l haŬZ BACC>C][[ѣGYz5C.h-KZZ/j3gPUUeEJȃ>}Vfyj222xIMM֭[6یRuyT*VBRľ}~Yz9s2339rM@ {=!0HǪv+H"MDc4v=OLL$55۷jힾ@ ,n"FwWbM?;NNNx{{l2rssE#cv쀋ҁe`ƛv}Q"##)..ĉ?)#`~%?5HfG e_@ `G F;@ &5"@01 v\\\w >7a"jLz2 vXnhd7a"jLz@ LjD#`R#KOO7_x@.[TWWj#??@L2&oBd,4 Ѷ^EGG:u*7of׮] jjy{{;@L2ggg&oBd,4 Ѷ^Qh4 |L$ZZ6\L^B|0Ϗ{._ H){G[[?0fv@yF%0GJJg!_`\x7xNGXX񜔲۫~_MAAٳg裏RYYiS ?>8pJϟϦMhjjqܹs9x ۷o'44[RRRBee1E*^):veocI^^… 6>>>t:ikk'Ns9"""@RaaJI^u466T*qss9ϱ@Jri ,Z?HLL$;;r㏑d6˖v>}:>,'N8)zԡgaȴqQ:::2Rv{ՏL^VE#<¡CL {799|hllTWWh"jii!77^OEEMMMZ;Q5][38;;âELKm6JKKMyy9---p)bbbhooĦtYc0t:yb|6:hhh0jjj2yJkFMMXGGMm5vd<(J|5=RО3ްGLt]f񸔲۳~gޮ'\%)}/.\0lF^`|v`?SM nPZk̙31cX_3 ?8N:Ell,Ν9{iz-gpyQ?wzHmwLFFFaaalڴ̝uppI-u8^ק XDEX<.{d2}Q9xyIաF[}[aߟATꢭ ]5"oLhhaL:kuRضh Uԡ씿? ꧻ;wwwT*E{k~(z=.]ȑ#h46l`s`mrhqpzԡ3^H)Gji7rJyt6˞477,zߐRJak)cQuSSSC__ɨT*\]]l&,, JIHH`ҥL6y6seѢEi欭5ɧbV^Mii)mmm&祦ӅhQK<^\`yڵL6XfϞm6'77dGnc8?Z-{!<<ŋۤƍôiwk0I#mXdEJU?v {,_>FRDT޵~5''(eժUcR|RJ)R*1Z&##|Tnݺ믿nbSXXș3gxqqq!33#GVeҥ<RWWǙ3gk4/^̪UP(444h'//ٳg=5#GO~M^y2\JJJdG?cǎqQ?'N͍'|NGVVmC[}Z=իZ梢"Ξ=w]Z-_~%&6RHC[g03YRv{Տ-~888}Ic٦ϟGRj*T*MMM۷ׯm-H{aRcUd۵ y~O/oړDRSSپ}?]7Í7T*~rB`(&b= {c3;'''Yl4?{"P w HG%22}B P w___>Kyy_8p V3q'! Mj&? ͂,4FKɚ@ Ƹ~\ N@ Iv@ LjD#`Rc츸?n7=&ocD<,4 Fexyyn:3&ocD<,4 F%c @ Ԉ`G FnX~\.G뭷&55e˖%o>$a,4O&XBm5Ytt Syfve V;fh$qvvf,4O&XBm5F$JPըj̅䵙!4 ͓f"ֳ,lFFj{ F@0 @ Ԉ@ @ Ԉ`G F;@ &5"@0@ Iv@ LjL v؁2x饗,q!Xo Epp0wXjB`(&b= ͂Fj{)F\._… 8qb؍g(Y|9^^^ܺu#GPVV6̙c=ƅ QӧO'55ӧ#ɨw1Qr7زe ǎjX()..f6P(X|9ӓ.8|0lr9otqUN8AGG /nRSSIII1FV_،3Xr% n޼g}f]#%/k6* 6Duu51---53Kbb"r̤&G7va+W&YXLqGpNTTSψoz,ɺuϹ|2III|{cΝ466ڜƍt#֭[r >]]]c0jO2T(f̙'|B]]DDDR/"|}}Yd ?ٵk]]]v) Wv>V__Ϟ={߾LXXO?4tvv̓٬YrvMbb"k֬M&;g؈``ܹlڴNc"F7vi򿳳3[n5 dhLqGψLFJJ 999&3Yh aٲeL2Vrss`0OS?SQQafѢE}v3GJϺuP*tttLMcǎH+((gy%((N<)\^~Lx vMdd$ |駒4޼y3Gaxƍr ,+˹q|HÕ].455&oF]]ٙ_WHKK ##<9\Nzz:G%!!aDz͛ɓ'7U EVUUQUUիW~ŋGm8]R[[;d>k֬di`i-/k6z`1dxxx6"mׯ̙3>+F7noŋh(,,\0}'x9s Ν2MKX+^>?1CP(8pпmxW9v옱nK.8fƌ={V;b̚5kTkecs*TVVrQc鉿?bsQ6le˨###\Ntt4g6ֈcC7͛7-7o\v&kco@&  WwhtwwKLL$;;r>c^~e233w꭭deepBvEp!z=ބ'ؤ\***hjj"888Hͫ޸|uu5 Rjjr9Ψjpuu{& mӧOc0$._\.ݝdN: #--!M[[[裏HOOՕxz-z{{%ATToMNFF6mW^2MHD{{;3gμ#}Vv>VUUijjӓe˖soƔ)SL. 7dbo xw|Q]]=Fbs7puuW^A.Þ={())l# r?`m?lv̙Ô)S29ₛ555c&3>>>,Z ;88 b7|ڵk5kW^%::&Pzhnn6_fS^3=:ΨWPH*@d0u2#Y* &i755fũSiYf/9ؑڦп0//իWsٰa{i㊊ vAHH3g$&&UV{ncP)f(^H)Ty&g?#..de__v">>htKK3 Ǚ3gl[n y^g_8kM}JFa׮]8;;3gyZ[[q$[({`(//(Z+X`gŊ9sN燻>SNP\\Ltt1xؚ%d2rt:4 *:߮^ o6Z^Ml-yXkӧGhh萏JŖ-[߾};L[r/xꩧXrqAxxxnS٬!R}l0]]]G:av{^Rl Ӝ:u@~Aqq1166vLcXKz ĵkclQ>>>DDD\ 66;gח/f纻 +Ww*hE[[& N-qE֯_OPP~~~6)y rݸqPz={E.l##[GPhihh0y&&x{{Ҧ=μ[`_kfrlݺuq; 0 ---ޑ!!!dffX%}$>₷qah[[ؼ~%Ŧ$0~ٿ?ׯ_g$%%,~VQjb3Rd2J&k3Hgg'EEE쭕k^`l)III!77wgnn.\vV~aǏF[[W\APJ2YY\\^gܼydG*R򲆔reggeVXAQQ-\n|舫+t:ggg֮]Kff&̞={x>''͛7h"JJJ {e߾}v/9sHHH|lذ7oJnn.:3f<(%/k6D)Y -mu}VXqFJKKё9sn-)6R^W||#yT*eeem|AM|)6R^P*2FRg{;Ei mxn ?555v}o7=㍍7T*7+B05OD&b=O6bH1I3;...Hoz@0FRG[dƛ@ Lnĸ3qOCSXXh&ocD<,4 F)efG `!n"@0q@ IIĎ;ptt[/Ž_ {+O=ٱcIII#J'88amFßGY05oذzΪGJh/#V{c ypfkwg(Y|9^^^ܺu#GPVV6̙c=ƅ L(zґBRRܹ{c˖-;vLP(,_IWWUUU>|dwlr9oڸz*'N>R#!66D|}}4449b()..f@[R'r9?PTƽlA eRذaAAATWWbS8/L6ݱck\7ӧO'55ӧ#ɨwA7lUk3\:Ï;QQQcO=#fDdd$֭?$%%};wH6n܈NcMHG*TWWuܱb͚5̙3O>:݉@R)6_~%EEEd~k׮eٳgill`00w\6mDggYP=eRSS->j{Κ5˸ǚHl֬YCyy9w&115kֈ/FTT8q>yy7;ܹgggnj\uV\OWW&o75үӟ3Ɲ۹SR𲲲ď# vd2)))hXf  !?cxkk+dggc0?)󟩨0^h"RRRؾ}*CYnJdr1Iy3PRt; IDATRBXX_5ɓ'%k ׯO<ݻ$!!O?Tݛ7oȑ#,Xboܸ\N__aaa888eat!y磢8tO $$+WRNٙ_W4HKKyKyqI1nqJիg?c&{_ׯ_'..3gh大sQ1 \tW_ŋIIIIf1 _+dxxxf_Wkjj/o vjkkMl/^F1ٔuf(>[`/ߐRGJj/̓j܁WZZϧlڥKJ/llF%wssf۶mjrlQ3o<<==͂yhkk&..kr!JKK!==Vٳgikk(3** `ܹ \tb9{,6R|̚ѣGٰa˖- r9̞=oX#..yNg7dC֋L&#!! .vXXO' $$vΜ9c`{`U)#_ƝpΜ9oAHH>(\x瓚ʁdlڴ&cpif$P_RNmܹsټy-9 DO6711lyMlRRR4FdeepBc^x|;:t^7!!!7N@KKq 6R󪯯G&P]]B@TZZ.\3j'''WGGqsn>}@mm-$%%XÖK.K\\߱dddi&^y*++)++#??dK3sLWWW^yr9===ٳx7M͛rRSS)--5٬p6mmm@8p| oEEETWWyG㙕+W7|c|DD&\.ݝdN: #--] k -4qO>@KK gf…`'99|xqq>-Zć~(^9s0eLfd`pvvLJEpBqo_ oXv-fիDGGDR lFͦf)t:QBT@`0dnשּׂiJCCIMMMٜ5lipqq!%%ӧB⚒;;v̙3aժU޽TJh4vڅ3sG7nΆ ػw4%.^Hjj*(ggg //3g؜F1>ұ;553>BKt)w_5dRR&\:u 5k,7U)үK3XwL򯮮6{}}}p5dcR剎Ί+8s w5Pq~ٝ4 DGGρ؞zl2LRt:JE]]onO5iOT*9z( z|QGyy9|իW BCC5NKϏiӦBgg] 6nHii)8::2gÍ&jfmj볹ͥ ǬXZׇ^)d %%dۇRv ёxN>m͛Yh%%%ro>k կJEʸ?Lvv6̛7>x>''4***9tM6Xi:.K N#++ wwwHjj*n_QZd y~M=?HK7=㍍7T*|c"4 QDd"d<ǝJKK;poz@0D^9goyMDdžy"2Yh6Rd͎@ dc|Q "@0@ Iv@ LjL͂D yl'"fh#L///֭['Y`D yl'"fh#c,@ @ ׯ7&[ÿ[TWWjq}w6&&&Ba\/s՟o6R["@ 1@ Iv@ LjDs?o]zW5T7jiU 9s_|C[(}%Eهjo7oJ@`|m_3,8YM9yȋef[u%*9K\/:Kv!p|V7 j%s(-{0NՒC]Rg=J懩 @ L5e~6Qto,W߱V&j{ ?L=̦,~ ߎl1ke *;ѱ Nۍg8f0n$AP|3|fq_C|,%K)p6braqg8:0}yS|(_'gfJL澝Ħ>NZ^7jy_Xzmd鉳O uzoҵ2 < *;)jE݇pDwƁ<ʟi箶YL A.Yգճcȹk`MTb<$7XE[:ƥ~lὓ5Ryy30֚5ӝ B.sV\Vϲynu##O@.⃓, qY,֭t:w!%j NTv} 'G9B ^WӅ'ڞnz:pv@nxQM77<ҍsך #g某tu;N U[ IJ-{^I;;ϫo5yu-=&i YʂO\ ?W|TKT mÚYB^Mkb:lHRJ8([lMkf4-*U.nwդi7v..hSZuIR*2ڴYB.B01>`p IDƞyJ9pp`{5E m:[\\a,e %; 3,8-|+>\U]ex^~ ZZL ULپ`5M>FW&ε[BfB!Ơ-W] JCohٱjMUwP׊-?k|Az~<.Gz]+<Vݱ&Md94'lb$qhE%Q*l]#2@8L$9zᘫJm_͕g32ÇWl;59m_Z`Z3#ڵx_; ʾǞ~]i68h/e+7+s>ZE4nS7g9fFCW85&")vo+ũN1IJڔg9E0JwZҵY&k`nÍ$Ӳ+g䙞Mg̅BC(/1CdrJ:}E>:K#ꙂEwǟ~ξ"( ai҉9DCcYu<Ƣ/ͪ/_iU(~^USK:0_i_w_=lOq¯wϡ(>f47;QSLPWgmefdgl?]*:73B!6_=(e}oyl ȤJO=6s3B|qMD%tFBq?bg m_-Zo!|B!64m ^~ C Bah|iMIENDB`termimad-0.29.4/doc/mad_print_inline.png000064400000000000000000000037741046102023000163230ustar 00000000000000PNG  IHDRT{jsBITOtEXtSoftwareShutterc IDAThZkPT~s^Y˥Jآ&Jb% J.iDco%DP5LJc1PK% (" r .=@qfuq9ϟ9}=>9$<頤% )-ARZc fdZ('ӌ-)\tt1A'k'~]UMcS-5˥FeG]uƮ:ck.~Jlg?B՞gd+ʺ(uS7 HxxW XbO-g)b(TFsJ/~Ίtm^;c}(s؃Ho:7AJ6czxzU⫓c8Tu\iJqSq_KήsꦹsTFǖxF¥\gҝqeP.i!YIj_p1_16~gKƱ{zi^f/cZ.d}ojʝ}xT[lF>xżU.Tm:&vʚ{xƺgK/ 3Q\c,M&={x,߯-8nj1]9nux-(HJ0&{oSC4v4jo%[Uݨ*o[kkmm|1jgLM h+=sL@LU[wɫë6bgk9s̎Hj[R[ 9<2y:wDue7ORrbbwwnmbyT{"/A]/LN ׳דOpDZeɊ2Zx']9xfF͔ek3[Yܳ z(ϥY*Z0]1y_Oɯzgޭ‚)Rgll4=Һz3&OXjb]˾#+=~i& eScIH9L̷D-@`0!RM1Оlk@4*<[cn%2%~f8A TMiWј:UiNi}}=[R>Rղwsmތ|.N"O縗 Ӣ[MJ⡛']D'Oi@6etjj4g植=o*"yLP~!bomHɚXFA @6/d?\.~`@|=. 0~zn׬ST{۳m;^!?ba2wo)}meRS}ՃW$ 4t꼖3lW\7xJ~ vPWTTDER -!'eG@ft~.3s=s  4Aʇ  XA+ `CA| AA! G.@A&&|=C m$Ohnܼ1QV#&½{{YŹyKWkvӏ  Xh.>]C197Q\o%[~*?,C-3:ݶcڧN],fq+PwY(om۹՝^`#Xk*cNdV7򅿕R&[Y.(Q~>7Puf)T%v K1=Oc.]K!ɥ o걕 @kg JP{_AǑ`4}cвO}[rg{INr]dUJ9p.)Hlm6/☛w}./)'&¬[YlynX*@ g[I|)'-LorsMTK{bl3+ L|lE;tSO*:&˗{IBVe 3|OHn9+fsbw]iNucڵ?vn#{V.ϴ>}0bt伀!瀭f: FDz;#StIBZn-zO~\]BQvΙ<Oti)t$Uw"aM'Lޡ}=}kh+brt⧜oV(nk_S) >2&fan2Tf&~NO'YstO% WA\ g(>RH>EQ _AѮcDݴ7%C^_>„ }Sk#?:V-%•)fHC! Yٮi'nV [L\^JP]nC`2kiY&y}`nq!h.־B4pw 05])j㩔Ei|ugbZx {; 9Ee #j=L Ov}]hڢc-cu5oz?r ZޑЉ]fAb9ObE{^-/΃s!vd ^cO⛙~'q{Q"wp@prhuxԟP"Ad %ez#MfEP`(Nᴺbn ą42#CC}՚,#e%ѳC(笓G̏Z#LA](CJIPYpT߬Ly'W'9PBZ?k';; ȯ1[)@xR_Ak=rco5ݗ4݋A#=Sl!A! &ҽ⿬@-$cZ_Jq/zM%cTx'δZv1FϬi 'Q`< PU4ߛN*8!ZEy7@ha*|0SOVtuv7_ӕ%?svP:$QݥRZ`,l~d숉y5N&gGGva~-f#%W>Jˍ+8PnpZwh].U/:9_rZ7O+/dQJso2{cStNH1ɞYZUO&g>}OF\rn' g56V> ec:wL[UgPv0]1ù`{|8}z{DW?߳=UU_ɕ:&!92Ud\WCg;>݃y\G%\%urގz=zAj8v)rZ~4!nA| AA! V>Aʇ  XA+ `CA| AA! V>A+ `CA| AAh?~ wd| /Pd~rT(ĐB:&\9߼'OjS_/<0ڴe4Icbpfgu`8G.:j w¥49p7rMZ&'䖗g?^Xe__x7(utbWz@4yҼKb;ȣ|vKK}-קEixK՟}(]w ItĨS"9i+D#|ц/gT{aCGϝscWdvPl7 R >V>e{"1}%_,8GUxc !ſoQy4gJ 4%͗&3wI}&P2ǫ{/1b뉹_žK)_&W@mO̙Q'TP*߹'<ĸ%٤sGQsD6K;XeAzgk:a1=T6UEQyPXUa?y1>]VSŏO/[W\y>x@л;eoy'!2ޝhY`@ _߿j!9=i^3$l~魑y<#+Ha ۃ> 6 sȶ.~|{S}YpyPG*G]~a- @Gyb/Gv,U@VylZJE :{趝U.Q,C= >l~Sݐ i?O~5r>xa ךZm*Z=݄W(e?uM3)NZ̭AbZ BCCm1&8%WcSp(,9ܟeyOg gJ8 λG꼊)BHOcp$fZ|qషnkuZ ^aC6[,d|h>uTGqn7|_[OC{S6[zqE\;v<6C1w~Xb㙔Y gY*RjmʭӽiJ-MinM 7)A(mvY^EU* @۬N˜'UUno.Z`x㫷~w>7kfO3[D\tKwwu8~#u'kiȓB( K©%47hA\G zJvHa,Mdj 5w)̱jeNWU@`SrxnZǏI6Ȁi#j:бQmLiKRU&;!m`ZSGRzuP@'sc:F]x\^k9j8 v9 $h\MɗUY'q$̸%d޴}ڔ齝W~FwSLoȫxkSény6lo_1oSod/p3%C1`QP~r{^G/sh^zHu+Xx`f<ž\=E<.~ 7gjnj*{ 8K͞LNoQ ߯Ceni;N_zx`ݒ0^O;ta:mE*NAjO;Aڬ|IL|sA,.x !UV.Jesv# H$>LXmU 1L$BkR$]'!>[C"]vxe;NJlI\K?M}ٲ/@r[][%xYSs!DI<|N%;9"`!r%U>T| "kI!ŃD^{+CG}S&d76AUl݇~9E'IHv?Q<|DNv9ہiA.1QHNhRO4!Ń#äv(޹!*vq]GsJ4B{:|Ԗs\{ R$U'Z9t0/rHD ҫ?aRZXZd-K~hNEA"ud#q%1,+߅P)TL"!OGi+jMԼw#[@^wm/.(/k=#~G|0Ɠ8S{щ8Lo#LKBKeWVEiYΏ_Jv Q}>\\yi 呎/N+A.PA@"Aʇ `CA| AA! V>Aʇ  XA+ `CA| AA! i5  | rmW>J~͏>?MI(Ya'>:- =L0nu꼛%9+d6/}uU Kg".2iM "]Gv0gͼleRޞb01:~9Ns8͏~xTS/(anB"tU܅t 'Mкph ~"*V ӚW0uW壂¨H% A!jtLqrפp[[یγ0lmwwy':{ Р{cu#˞u j9 xSO_%|U.cD)UEؓ]Pڤ'uU2[~mX}t V |WPݡYB _zb'!TPW|}raGd=;泪9b08aKDC.RxKw_Dy{{5#b+^"8 H;?/ `sDu!)K3D oy_^ƆuU[g7 /1P+ąV 8vR=U f@-eTk?/h )^2LcŢXǤ~K{XBn5DDu|xV`j噄a{q,9(u?&{bVa2azse?8s慆puF/Cd(p0cjTcfmvsC!4BSQ2\N *h{@_KIS^vd6F:X),\dmZuS̼2T o9ri3i=+=jg\VٓF5U^=HMÑS6}~"Z:yAڜƿ0F6@i#dui8ZR)U.GK]얜&KxO7͟z@*3T[S+h#8SkɑZ׉ֳaY\J0'Muy '`⺳7"4"uw 2`sDtV!bxKw^"ͻ@" ZԆ3u9" 0(U'q^xBV1\۠ZZҞ/zҽDw&IVRAK=߹2; ҚjyLؔH[M5 II`kw{eX{)l(1Ȑ0fs|R)rJ~FXU!FнgX{s-k@%BԱiaZ(ZU;jjGD0%E)=™/&F0F{G꠰@Ydr3(xgU Zo99 h3U{Z p\SÓGD%Y`;v.XÞEC 萐 d`#M.:("$D;Ehmr][fqĉ!v}uY?GDyR09"醈-}%{4<ЁavS+*O _IqI_FrV1o3} O2n ] V(J.uJJ0tyuמT5:ӱTߴc3g`(kV`~-Q} b !h [w(?x7!I@0#hjޔy`| ״K^yT$+${ &5V:O'__բIm\~(}#k3U%޿y:aJ .IuJ{v"8?eC#WT䆍kЁ%WN۳y#miC }f^i'%Jki\Q#:_'ts^'9O/4l2 ?4Ԝf3$=y{>C] O;A¥ВS>k;1}TlUK "p Q; [Yc~aT))}Q+ !=ՌE8kb2 ˆCuΓrXᥙ‹]ƇK.'Kԇ.B419Ji=6x BbGO#-V}Z9`8q￱=LV|9vQ*핫5>4"B#X[hz>m!듧*?zyw !4괲/fw4_7{`]ʏtg72S4}zAاu$}qI6ȑl :uhcٚeJm.> ;gMa 9 1$)t4 Wi# #j[@-jVl_&Cr 4vAyCykb"$k|(XwD}(,n)?|Sg>\׆\CZbSͶd$CA\8AH֎`7:Ckze5}93UơyW4 OixnuKr`<0^w[ *- xn;t•Wwr Fڵj)G*n}9uA(nW:J]\ )r 2 -ǜwM\iaB oޤ׻dHz nS ꤟd2ovEO; tHt*m0W5=[ZiLdA"4d1/DY3v4>XE>ԹY@edk=nPXl4q% l2UߔÐ{\'"hw* ř&A@}WMYhP=u_mYrF9+z@nH8l?orٵ[3"#ґ̯-j\ecRK}_}˵2BE?IHn_#EXP _7n޻V4/@!7#53#Y"GvZS[Pck A(#ȌQ?@ T2bK뿷[&Cr /DY3v\RNAJ8 cLnh]+F'/jic˧ эjI!Y-Č'9Zʭ5=4lF9_ƻW'2 Enޠ M#n_5ލ'L2YuiRPCNW/zkf h >~((iQA$..9#pUZaz%'B o~]THZ $)z"PQXy|^0O72C#ob _2;d]ADQūD_cu~H,;}(),JNj5OJdv~`9m Q"=qzװQ ],P :rD-EvǺ[]ua̔T2I}  vz76De#?UɘdP^7Ip"!dxb Q+xω"둴uDH]\ +r$3Fj*7jO' 6+|bxo'fkDz=)HZ N2oRPKz`ho@d 9-@)bOD7WɐAACTUͿ-9av۱fueͻ9R-&w91dNyk]C1$53Ho%wQ)(i|І7Ɣ]_vGTH|drmw;kC!oJ]m ?R-h% OG4ϚRmѧLǐ5CC֔_dw_D8}~b`ʵ5ζkܕ򻝨Հ\| b+.u@`ܰ4WW#/=r9(R).]ї2I6[6.Z pJ+gʹhRw4 |~tg[TR矛=WxeKݧhJl12Aʇ  XA+ `CA| Aʇ  X΂^xǺ˰K ȕWd۟Z_9Ͽ;ڷGl˹Uxժ懡A˧qta,szt rUV>.4Ad soy?  $oߍT€[?Tk/%6}d]3 8Z(c<-c)ܺN >_%Px]`-]a)|B/a۞p.Om#ޱ~}s嫶Cs; r##VaFADžP^U&1e<K&;s[keΨ8{Еzur|eͶB>O4O^)cnqtqvUJ.vfi˨{9}Б:%^J֝,us16ﮖEح5 AkmPVEB. r[>7͛.h̑`تZE> Rm}dǝB|R 8]'h"W%,4P}@)yS KP5 d|B0 5s(S?R?2=ؑBE9Łe͂u& RKe>uڸBV'wn=Td@PT0ЇMʧpEJ; PS%[G \{{>)aoeU0Z1IBi M<Aʇ  XA+ `CA| V>Aʇ  XA+ `廼QtwoȜA޸.vI&⧾`?Y]W,}O .~^Pi2-gk0p󷧪)@] Tic'vCc!wQh9qtDng2xB6t"8zL@\׹.:GW?l.~^stвs^C""FKi Poj83@{ ?R_(kٿ|{S/;A.їzR~`;w#u4vތaz c7?n/}ILUUmZ2(ZUd7){uEacf6oϪ#oxU+|gٺl9wXӤyۣ}+>zo8Zt55x{7]5uv蠮#TۖJx!{ u[=>5{+XNS;2+N&R:ގ e%uCqG*9}~Qn~S*W󤰑'O}|nK~L7!?5)ue1 ̱xM9Û$'WϺ頮#o)`'kst#ܠ9A:nFv_05 ûlR;"ׯ$@LuE=.δ9êu>' 5NhRi(dv lS @d F'p/ȚlzG_כyjg婯(N{}T>("WjnNq8/cinY,hGt qּB.h騾"OlHwUܞkLK Au,P)U[mʩus|i25$CuzV.ש!QYܤXF\^T%*kZDю-Evm޲sz@47M5f><>=ۋl ڗ)&[SS|0^SNv;ݼϝQݺyl2YdǦP/9qgX IDATw%~'7bdY'#MvCzL @;W^XaL*_M߷w-]zQKrn* P:fu yHtjJi}ɐk)nG|k D'm@  E6'P:uf$IḠEUV\$"-P)*ڐ60AZJ ؘt]ݖ]^bnKM wk=}N-,6uDT }FiyHrJ7tP^Ϭڿ%}d (6KF(U\ rJY--G5>WPZ!=MN>H\MɗUYg!稩44m䀐 yr=JYCܫwGCMH@6tpRw>82x C(ua#:~Gs͂o$1q]:Z\{>9zOc&qcC7I{\ Jt 5>qTV6m8,vw윾ڠ^az}^7DzΜ_2.wLb/?y꭛sƟc3t!'yydYϿmTW{u5s x w._и뫏fռ2c!q6yl7 ۺ__\*s \E[K]̑Ӈ>C*~bUȝ~o.-.#*X_snvKuC[!&1>y#PWxk7Gu\!|C^]ǻ_N\tXYuI`o'Of ţj@. .wwP߶xE y`f<ŗ}9 hˏ90#/W +'dP[A^ݔ+X0>5вwq .NfQ}zul 6OT v"+mU4. +Rg¤_V.WIiUÖ;2= |u6g+sE.>T^a/Z|WI\;Cu~*s]H q#.@.j  u@$W>?T ckg+rOv9uqJN-D7~?x+w/HGr[$ }yj\c{t۰1Ϊ A[:Awh*k92C}*jHoql,r] 4/'I3B͍na|?#‡z_ʉ {~>ҡV,H$7@a, W4^{eAu:k'R1OtL8I||1=o!QY4iX:/uR~Vf/X4S29|(]I]Eu( 'B_!+ν\{-tdm/%H 8+‡#B"i3pxcLIxiZN{2fWr}^OS>MWoM P@Ʋ5HfxP3toX ݮ_ۜFgwP}hIl` fr peSwn|w^bLR6ҸYݠ؇z;[WH/ޢ_M ~ˏ+2y]Eu*SaUU9UD|^*EqDm۔ ȿ*b77j fmt%~GkW1Ҹ_)m5n,VBy -{JO~*2!9 Gd\p \26h!OZHֿ{Ps{1AjlV/O}rRh6Mӵ[c& Y8@5ۮJdrztՉyo6Omh yݵl08|Qػ(?^&w~po~߀6 <=L컾]51EOvk(:p J dByW葧zW1(N-7LghHstĞϓ['R)}(.& 4-#,Y#w]ko"FK Rzw8e.$%eYG䊨(rkvoe{^3["W.zB*I mpaeg"^mHO2Bi+PmY[`"6l=N&(&7ԩQ~u^˳8M4u3tۋlɟ*Z^sDj Mn-ԩ]zD|yFQmlvlz%ޥIRD ԧϧlOy*"*R$)@zO6&e l8tعsϽg|EHJK1?5w=lO3.X%K"޼t`jTHPtDEiP-'W`΢X[Kzya]RY%chM>BJ//N^"kg8ى5JTt>?.'V1r!iΊU x^~RĒpu*|kR,p?lʋY|-v+xóE\GD9}͎߾vWi~1:Qka $s-\y~^46V!X/d8ޔ&{ς-\MweS$8CfX{~+CjDP ܶ0x$E o[߿ϟM&vn$>CÍutlﺵ{O>?$Edp+|:? bR"m>y_8qgF1?o]Xwv~2)U}u[KyT H@uBt};~~,2;FI "%? H7y @uJNFu}!BD]2~sWsť}pP '7N/UutQGſo%U8'}P/eAIYxtC1"G/+ ]"uGG !щjnDĺi B5kMc1dj>) &dȎ#"T}8ϟDfu:tt@q\C+538H[WUUIַV 27!n|͔opCIRNL>q`+Py~VTM}}O}S`(f8|7|uJV׆tl{8Rq^OXزr^%~Zt`دY;|u#"m*P%B"*KMqCb2 '*5V"U_} 76]<%&}#&"X'|:1ۄB}+YEjo} k9*I?Tc-5r!B6X`U)}G/[iɒ- -i(VHIkM^n J\ctuT@ks֭oDQ)wrRsێ 0y85Ի8ZU(J~Yj{eTKYL s;ʱ(.>qZh`t_?c./"=&XgTZ3gۖݟZH5bj) 8<ڱ?q__>!Zq]z29_oQ[TV[W[)R} 7n .Da8*! NY LbolǺIfQ:-%PFQXi =nT)Ц1}C},T|tﱟZnq#8Ɋ5#⎅cH؁܌dX'}omx?9rԌm+{E8zYY K') :VGk/0a(,9Qa̵5,B 3K*:X v\rm3[ H DHaɑa uB49Ims 5ViQ7Hgu_< k`eyo9pH`୳&edZBIG]sf_'AI4<ª @ ! 2<(f~ベꜺN_ ?T z!jtugO忕bˡKŽ=}%.Ņg'ݝeX[#7ÿNd&7UkKd\XjJ63< ?K*[YV8~Ɠ uڨ7rc4*cVBܐ^dh+Mx%ƙɦ?y g}I;)]!ۄu#.Rc(0Ի'"F}1=nmx=":Θc&-HX~XV9[gYW/g=5T<!Ԕhт46дXgO7{;oPཝ N> {>P*@|T>P*@T>P:|dGӾ,qn7p-2qp\'xbr~AOJ64^\]K7=Hq9č3+qR7Nčoe* Ԧe/g4{>*O9*WLK.nU+}oI4l=>}׈C C@If*q; @Ϯ|2QP*@|P*@ W]K wk#d/>o';י`8pTm$T]&j.^Q>=9r]އ=7O fGZ|?qu{?'p9;j.ʒVx]z .wBgqOܘG[}_8gItѐޝx銁93N}b6k>|I Qʊn5c62F|̴c'AGuzM#p fh!v B-'壏w".m\^=Z`.~j0m?jdBT[_[j(-m܎Ue+ }_V2X#gw|1yB?em7'*Xse6o$'?ؔpZd'P+D׾mJ_K<0~b<1sqomg7pp"t^v.!C#LSܚCvS"wNp~⬓ _| ('>,@V?1cz*ًXΪz7~ IDAT)O_s;@`<|RMg$_gld#S[DJ}s֍-~^}O /}"x6kbaڝz|jy<5hTCyY;xs>6S?|uIǟھ_zg} r)_8Ǣ+&WN}nZ=_hiצv:gQTg_+vtD7owt+l~~ oYsӎ^x9VO % ='d?`&㹨}~a>Ib$מ '~wÁ H11.᭄MgIxg(&/}۫&|身 !p.8?pTey;ggoFj'v0U2֒Ф.:UĥxM!~)繝էnEH7=y1ɿܧ8S[/x *8zphyn$Nn(g̿|88JN|[,R~ڰGFR&!j/^lxbrzxb_xzspg1:_ 62܎_Nܬ<(sC{sk4G7eq_̞]!w@cv%~aA)g/(kcΚkxxEDry7udR9D 5x;|Q=Á !x\޿_rp񁿊m,ݔxIywL;+'g3?g][RN|3QZ wJWQ\xMH ۹梌Ns֠i.VPQK[qܖ24A(fp QBwR& 2d34jR4O^ȤR`xs:!I۽5!ޒn!) j_$k98rG嶏v!:55GHKtq?Ӧ88OYȼi+[Tig}eI5H+Nԅ:ҳn GV'yKJni63-%: ,x b2(9d}VxTiASeXXeXq,Ku7\_$/̵!O6Jo_»:)kk(M&5~:r_nYʚXvDb'/]~V܍E6~yFB5 >k{jYs۞|9k,$J)Dr_5yn>?;k'oc߈;{{: WÁ& ,!<8lVwJXiAŖ|Jʜ8O>a}?gZ+MϯO3s|Bnp;}a\cvyQ:#xj\]C5 ~?^vC\&[Ns}кŠēdoٔek|O㤝eS Znū32R_;Wto?GM[{4 3n-c¢IT'9Ǎ s i'\|Ot 2C^;En㎮ S$6٦qWf'NP6q9;jndn%~ni5svnͨaz8k:<ͮq=iw΄:nO~/eGMG񶛜,0L#g89<|^kgdcs~Ck݀&^D히G{n'\P$||fF_(:,…?8%Kj3-* <-ۤaҮWNo.Cq^^үWQ$0EbTv+V<($|j>+[`/B Oo|tZz9i?KP+fVbm,x( 'apmgЩ3HoK C, B2/ ΠR9{U;Y{ɾC{m3xg0E! Κec^[Վ27mE a^[᤮h;zxV?Oixt:^7쉰W;tV!l TMmңTOBuLXetuў+ R#[e2lf:/1vNB0ctcUw㉋P?^ 'uEݠSsV3n}-r_ @`/0VOIȑ {IK%pĮ#('@y).jXE!StWz{tOX;Y')OP*@|T> A/ `Lɷ%Se d{Q-$|g9#t{۸AKnK"WU;#37l+t@ J*Bz#>2cG2GOU9׍cz~S-]f7>Kټ25_w>>pNJy0C؃9qa{K;2XtS&1Ͱ\_Ս Kqĵ *dhf0Kb4\+rU\'2u0q!6ϭ;N7aY4H+S)[S듟sqGMҔOI}kC͇7o_nD~ATُO5Axm"$p(Rn.׿5!*bC YajW-ٶv|{8i4 #-^Dh.Z6ܼ""4ۇT2jޣOLY[>H7pI}u2m}enRD1udBsز=wLdU QQZi}Gj- is&ԔT(&﩮gaPtݨ&T6a9d+R~[Q N;nrn9փ6IV_J\Yw/Qwk~}~FUN {'xuAc'K%vm/C{7M;Cu/ CKy /"q-=40Ǯ9]ϨI+w!B") +)\'BPʚ7o/4B+pʇ&6w9nT>7 EWrpcFxf1qS8dvXvYl*d)G2.p Ԑ޲mB`$&bq1ے&O)|Jccd'sWrm8K.!_15\S%tC\{\W\#lEy%vPCe C"'!IYݣrHTЅ*1v_jP!T*R $^r^,"IJ ˲c9~3"%b BsJFMNRE%a KQG 4\V #g-I RUҧY:3E۔e;7ɬY>'ƪ3~mጔ?5qv[~x@c}ym%dBd>yˌkv08/vi5 6ITBNJ8[Y7%*%ZN./ r!C9r漅j)AOlAq'~9us.-gb8 3sLwBđ ֬M$(qvR@CjBo %fВCӿwb>q|g[=xouɰ(PÞn3e}q%,TOpK<+ @/i@`8{EֱGl_Qo.?.B|5[w_-XIZU.ypNܵe"Ǟ|_X1\Mx]-oqX #vB@ɴc9ߖ L>mLR՜9c~gNO|Zr4ؿw!+^+\-Ec%yʻԻkS*z#p ^CIkBJwow><`}5DZ[wXڔP!/;v pչY%0dbM?TGn ,)ele.i11*WMV>s)Z[Wqڮ?zzXr(`Nd!*&k(be/gmW(z!xe&v8HunyYS7Vn'r6V!<1 :q\GQ,W߼kT>׻Oq>C3so8lpqodZC&޵\J矿G!$A!~֚sg={#O1v/ "E؆݇ Np32dਅ3 6E8~{/ne::-` u72́\!a ZByƭ\T[52XHnBkqH/lӈ1.pj.er3Lt|#ͺg%ĩnoŲbgrkcXd`|03OT\\jROSRrQhh0 Q!LϺl^T`+kɯD̐s67Ҟ$v8N;o\B@v1;%9/nel67M&RI1wB DuTfd_aZՔF ^F.GKp9\)#ÿL!--bHB.ycJG>t:3+^עQ8!ɀB7FsrZ[Z-62Z.E.IqX[9֖8 jJT>]?JI" $W)svC"4$K\hV# z_Wx#O<_a/:W4 '3$4?Fmv*D)AFTDvMo!S+IXd  s24~` /(bog ]}"5G\U!5v_eNH$.&@j HHD]p B2<:e?^ u'Fae&v8:#!ֹCr)/(w :ON4w~#nؠ1! 2(4|1]QKoy|?xXn||Nrn.W!|@xjC}G@B='fP!tsC_2ӂRBӞݑgEHwhVfHp2`1d]y-gY[2#ޜ+|dqRW(b޿`-LnɁeJQTcoLWjmjxżRBG ?ݧgpRo29ݍ'BP)z jxVs 8y XUSi''w9!&(BFPLAEU>I[ަ"xPuO;PT>*@|T>P*@|i40a%J_|."dFVٸn2!<3׭Flȳ6J%ܾxR%StIsb2nMxw)# 22&OmvK]|F\nvZ֍IJߩ: w8]yW7J=y#NgԳ]ax0ɇpA2Ml%jސ XwCg??]kom$- IDATu!jcM[.,=눡s7mzK]|gm.uW7;ulֵ}>;Yքœ^xzөsJA=|>#*1uv4Y/!}C]n3\uHo=B WD 5ݳmy׼悂*W箣^Q.u]^^u.uo7ylouTާwζNYƳ3gg]ֲE6|d[Ukڑ!3&=V=~j$/ iYT B%?9]"D(gMRnk6j=VL sܣ)}YnDYGܳ86Dd:sB8C<(G:rs49P9_\+߻)x ;hHwRVk:g`CDVGЮ>2Xz`~aGu"@?s](~ 'NѕodUx7q΋#0=ڠ cns{yw2jت%*ҟ~1`Rմǚ8ΐC6u9R˹Cu~}k k%M/s gHh"lcG<%.R3x]ә}9vVIe{7>QE-X8/@-IRVo{w̢$O!?W=]7Iu;;7u9Sы$"{R49eodT0ɳ ?\Jxz+wVLpm_EaپMzd#Q}~PYCI6J$fX" xEO;s&MZ2?fqe1M]|Ô6sc{eN{z4U~ $J {Sg773Mʎ~G&8Dn~R'B\[t}+3d?|];aՌ~&ك\qCH"0iyT}nyų%!mY]4coC'n{|x;6?01ŸւB\Éi慏;B>Ȓ Ě斏qĴفKSҤq˭o2HdacoUԌdȎmr1 ?!L%#D2 !*f}I>5gWg&qi>C؍Р#3[)mppM3ѿXYjj ,m.7M pCڕers#+Zε) Bֱ~"IVCxp/7=l7y$ V{"0,p._I]Ƒ{bCin< o]:!$;)޹ 0lM^Mλ\&-_8[BR-wCvZeE#5Ags"q!ImfN){"ZlSU\"<^u|G69c7F{6vn;v]ӒҤ.TmtqA* Gh<g)-ɯAţ5!DXtZ&oxzǞPƪKÜA?PMUADoĄUZ#98,%N:E`#MLj Q[[tjc-M c˒Ɠ%[ZQ++.r7] "]:!ZΙl%X|JvdHPXUfyZJ rxIP$!xb#wB"*Kn?Vsgh P!bȻ\š(].qCb2on6PFV̈́0BVM\`"xMLϯg3F{6vfn⦻7p"]጑mJXg΃5!tn)ԥzI D3:m3#CR?/rIerTN}ffHÒoxpCu|a&|B⺼ YBHt!򆬖(+EgM4P i8/\TLߛH$7gG004Hߠ d;n6:~:gq~".DQj>=rLBnB:0Dn8Oe`UײJ\u|GOu^NCE\kC"HDH"nNN zHDڷO=YBH+kj^m s_NExM}Z奉GMm<^kCZ]#N>jQBM 4s&ܷo9)b7yRaS9F'|xqzQcq728Cϯf*o6nlt67uT13gMP5XE,4gȁ*BgY@ 5DFč1 sS7ۘr,.i Mhdpֱ~*BGMQ,q![C7ePxmA~+,yeĘ }SK6Yr3P M9q۔ ĩo698xF<-.>)ԏ|-'QNUae_:sv@qmJ?+8eRs+](]_p{&\uBz-ords5VIWn lIns3C]&&\ bLlM~_r>znVΖ6=3;.)k)t[h ÜZNOtp6.;߶ȅFcݓm^3Bh? cvkaѵDWD9b@`zdH'MWJ8 Z$nd+34ܘ97 KW:M?.tX(!p1],!?YwNP;  D M DQf1яT1de^~*ʎitN׆'t6 ]"XCR-< j/ab"|cd2pcۤi7GP>QYR= 6/I"';§b"$F׆ЦbCmJ(Ř:wBSYTgV>tKtol'Qs Ŵ* 8<ڱ?q_.|%^m!ĺ6/t6y QB.vz 18 6i7 Yg%gU%'q]:[Lk& QB.?ʢSvq=dXq\#[8Hn/ݑB ?KOx>[[b\6቗g~'JlS/g=5TvŦڌM28ĵHF._K1s-ƴhJ |G=QY).0ݴDZڴgI!3+m/w쭛^kCy&m=Az*X# 6lÁ?7Ѓ! ծN襀J@T>P*@|T>P*u3b U]-bD:г+w}QZjx.K]k_uĐp#8 a(yC$#FİW2 @mZr&I#(fv.x -&7\N"{|!QJ~F!M?\5ĩ}"ёγS0 5ۛIF!]Ў:o8p:Gs5ؗs4||bzzdg'Dowv|r F_z DDk֨bs3eUJ)3M1(OrO#WO,BdW=iMǧq2aqQ+V(_r0#^ꞩmi+ܥ˪X7i('8u}QG;}~E~~QʦFs2 pR'"hտ׈CnuMCFM{R:(#HTN՜egt?!kH;B,ls!*½(Lw 7VH6SHz\a/igtCg9t$ܥ;%.j9%"H7}Rv&%e@!BʩX9kk$]B![GM#X :k!bN'GȍԨɌ\TN` EN,b z=!n\ A#4=MG!t#9Mqoi!].GG<B14L}N YofhH2ص(, }p)}w8YpږJW^>ք˩>zA&Lj4B\LuNŢB۟z}?+J{\@qSYu+݇c"f?=c}߄"3 u|b/@}jwށQT3K6uS !I .M D( RDHPR'evGIr'̒z>ặs9sn O ݃4wz]{0'“c 7LZ T>P*@|T>P*T>Os#MXzJ YW_2fˏ{C܈&$ _/W[ r&o}=FkœUD6@[s/F?go*t43)KmOS8l(q^c#>{gsZ+g7]c^r67]C=kj{BE3_]7]% vەf=A+c 1O9;ʲ?GVz\uq5Wȥ( 6o0]zCQVB E#DSVՆP(!8y|6f|caR-M "*} Ls"r޲1mi&/\:uء<,~ch<̜3iL 5/p' r^:ض1%V$ 1 OZ7iJ^~IYC3J4pļC)ˈ^)UHWNo4vtW/j.q0Fu#e*sA5^~4'p72Ȟqn#݄_Hk&Ve+"$ΛB'GS$oOV2MQ41)&Fh^!a6j#put 5VM7|pѝ,x\ 玾U_|K8FFl7{ }i~x޶՛St/9gH㥘lIvٟuw$y⯻B/.mޞm9s~ N{E1z_eksbe7A7uwbZse/ry3ޙ;pX=S3"#6=&7vU !N]r^;vbk낊/!Yw|7zϰy$o\>_6}ɼڶLtlǫ}hN6i`_fxxc"ۊ?]gԲwFFl5w8%j|Fؖ2'F@Y͋:>Jº8e4VSWLbTkcɏ6$ǚy2}2wҫ%VǠoh*)7啨iVom5$p8p_>]pE`+#TxʅKZmsS:w^^JfsʰP;>?'%;) m 'W>]53N|@#A>0FGH(r p ܚ]tirlܛ731IWn SL }dkG!,>2lnk7۷XBvV'̇hՉ-;E3f~RD*ovg{k#(SO0EG>6ftųen8Uj4EäsKG`uܿX<`Z}q玦I誌M *p//j^_ $n /̝ñmI"iBQtbvVgZFȽuI3u1gơrK;ΐ{F?'uQ*Q^~4m+2:F8GH&g/7s+6h9b27g9f{6汦T_;铗%qoMl"A_2~H sbn>bqXe8n.Ιi#WB@m}4Bu=-c@$hhE-O݃4whNh3xWm՗{KĕBMzf\&'?՗>j'K# *)]^o$&K FPMqU5P) ~yoIB8<x"ѩ( 5+I݌1ǒ :Lv%yFB KO_DqQ!1٪[ON!#TZ_N^bEp8hHW2v[>|/~=1F.4YOJB{-] 3jΣ^M ;$|)Ky2#W&PlEty~?O% "$WAquT"LjZAGvK͠9UB*cwqVs&8T>dG8i4%,#.EH <O#;i9A JOm2@kb xʎ^;Mj~NP\M4$Sn'@T>P*@|T>P*SLвx{{J#=qH&d{+>CA^(pl`Hn^ QVxDu6~_>PvhVWi” WN*3ymG>ɽ"EN/₁BHҶq}喒 i&ߘYs§//);$B#vx4\0(|$?t:i͘-l0fA (MUmk$P:(JTgv '}RmX|/),|Qqu}'ʛG.=8:'9Xhׁ: ᝷˭Vy?7ub`] M?opӽ /aY<./T3v!χ*-ҐG~kv: ~ycpao~/RE\y{XUZ3vJ%s29upLԸk\1TЛxנg ]\PO&Hb-!9fz](,l00${O鑹s6 9+6`sG0װ?yHk?ƔZ?誊Zown:~MB^|)LP _pe3j'6C>'g;wyܡ *{gIEl ;*]B\_P}sMg侀zj}:p\#Bʢ[(fU)dd&e]ܝʹS``4Uj M4JvAf(u֤VVkk4+.H2 `+Fڦɼz]?g%NWw?W>G~mXH/ 16Rcf +™釐^Z|2 !*} {<3ܾJW'9(##'[YɃG1=Jh;4]S7OVLLhd`ea}:xx$rsw2Z-u?ؚןMmq rqr62g{uʂ}J&폎,&3#I܋<``tnn$*[u10e2F%"D!DRh49$@HdbKFSܜ7i9s`ұn;蚪Z+RS!Iڇ/h0;Y{=7졣%:L"{UWp@fCz'Eh1h0t _LFB[myFQM3͝5GW*mz Ҷ^;A!"(MÇ6 Nxy쨈=,2rRIΌvN^8WӚe*U-yĞb#r)0ke᲻wa铩ͫi3!Duc+iýp@=0~W7*VQo`0gŧ *631 _cI]i씷Hhma7!УCnLKyFK7_#zʦNVޫrGkmX>qw)m<P( -n3 6(cNH1oMfO1P*@|T>*@|w ym_7čx|DzENVtnªS,>{d!Oބ`hz<kS,o3YKbD!$1J~7*=n~ML>큩M9~P\Zrǣ^`y'6B>`#< ܧGVz\uq5Wȥ;V!dΎʆʆ;#6!(ДJuo:Sg^7?rMySWM7|pѝYtOӝ̜?ؕo,/WJ1OͿ΋;u@oΊ4n/{$qڲd^&sT VYw"]NHG̛=<\p)b0#Yx(_&žkh\sqW׍$1xӜ9V\sH!F ~狀3;W.`W!r>_2(+2!y1o0<V5^Ozg$tαm;cJH8b~C!x'}!!;qpjO}F.~98O7쉩 07p9Ȍ{hsO4}D7.ngEty~?O%::|ƛ݋]GΧ55zyC7ip> Θu®'wqDƧeTܓ[ci=8|ccIϲe8cٍ$jllsf&DԸn)/χ'n_ȷKaʣ{/[%UYiteX3yeȸx&TO1TM+B6-ᖞATint$Y.1}B%bJ-!l;oJNa9Oh&ԕABVU*55ƪJL@⚓͛  QBtb\V!¥R Hζ)IXOY>5.U ZBoX8!7Oc֔UYi$qp1ƊrWo mE'oOs'8-jt͈O n1%~gI6=lmg. PD[vnnzBAŻ<'ϪQC&-t&.1ܚI"8KƳc1h$R]*j]skRjLubVGbO2FiM# $qIc]}#)2ܖ5>LFc XLq>nE&e2xQ|cV,b+=WP8@l+Mڼ9-b'dq3)Ζ*q о-p+?ٕ Q#gݻu6c6vǻҩik_Wk6i^53p;h ftOk%$B4"DR > 1t "^gKל?ݖǫߎb\\@۳"|C!B!dCL!' zrДzSnO=J"g5+:4a[rK9+6<ý9Bَ;Mw?53>OA69}nC8:l?y7mOqI c,A@<,cϭ E)?(Fck#9yvIIroN"ZUf;>pFK< ;:]j %-> dR ɍ=Ya#R%͞=B^~4޿s%=ˬvMv7#DNl)1/"PxtWsG=5e6DH|OY?Ȗ  p*!p FxQU`g֯|}i;o| IDAT?4=~/>,OyGX?w>#i~sAq'<)@ +!Nnuu ZO\]iѯ0'3p*"]"#6=&#U#e%+tIr1fqg>E$+{S񣦍O>\Q^aC!YFf ɿJJ6 Y) {V;9 G9oY}tWKJϋTHo; Lj^!D3*i{Dyp#qɰG4}lǁIHFtmC"$5:1O!a!^z72 LS6lZ9͹z*~E>uδ1^qcNLH$bl]Ŀ5VRF)A8YKj BT8jTjK]]:jsz8;䧞I2Јї929@#>/[unO*L׎^21 r1 $cSgMc34B6m3Y)$|+Tk04 lb置]z7ʫ412i$pv(I?q\OY5jsݔBGƜHXlfB+7tw[lp:Nz.z-1!]kC#N$$w):xm|"۔n'hrӨ=l+bY1 A#?O3F+=4Art ?[^ySM#kή5XXw+dmAb;yI$5gl!BϏQ^a#ޚbͿ;tv4O'}nAJoDm)۷gS %rAeN볉ޔxn^ }Nr]WUѡwࢮuo LxMwB$FTZ+tu P ϱ+PBH| {;a[ˎ4QOӵIj8 Tε$.\:*Bv OvzqQY}ψaE|._#ygOFRy(NZ::TqւB"w?/mOM /gAyZuG>Bo՝mC܍iUR ;_Ί=ViS)m(M' ZdDb9sgiUR-}g w/I -sUiR.ϞK>QC4"BT&MIɯH0"w *>TҽGx^* 2=鶓Mrgb#;xJڶ+yh{x(r p=wّ&D:􉕆i T$iԤkhd|B~۝?uFOgNb"ψ'!*mGls/;q897+|n-YP qN| w w{1N|ID}겪_PH)XIG2ۉOʂ T{5qu$2uV֣{?mi "^6B.#AJeS?JCy NU)-Y_kvH0YmY}g7."̷k[BR_Vb=%FuޭS7''XLwk1@RkB9`ѧf Ù՗~a%gIhmq[YDt׎Q%Y\rEg/7s+6h*4!ѰOVYB*SגŃht;/<K޷;y왽36]G+FzFy&8 Qڏ_}pmLTܻgpf=2012.yreDU޸4sKe9kYuua-5W=s[+m-s'zYOz4qU2LMVLրK{TL ..zO^QQPS^16ÄH߈ٳII`;3UtCZ[wH8+uATIiǿ3:=USk+X}`Fg S1Wj,KJ8p!R\PϔR sk섑o%)"3XpΧ&=´1W%xRmw;3Cg^!D8]Z3w[w;ɀ6ژ\ML8/Oʭtj _y3FY㾿 Z pMĴZ{: +JjQ#n|#rj;9}*&L^ve]T>xQ]yT <>Y̘gϞ̴逧}m'4*EP*@|T>P*@|T> o~.ǼF0z_Mdh`݄Lwu߂B(r(sAc V K!\ ܷ1ĮHo3W}A0Yi[OYOILP::=Kp0m'mC&+2Y B60YيhBa߿і5&+nԊe̟["lܜpl}ґAm?};8xm.kXd~~!~ Q뛵ֻl8϶k{~LQ@m~;DWX!ҷ:}l!A-1/??ϡW|ӜQ~9=qx- _$$ίK2N3;$B!2vY$ۘ@d\ю!al]wO;yRiFi6|S|_/"}p{ = ͽS5Eo%Q_4|$X|gbυ뼠PwS61IdH-Q6#Y;'bxvnB!>ZV~]wbTZ9C\=-ЍTll$~T*J Z#(o NCPW[NA h5QQ#XD|j|U'nM%{Yҕ35~(i}Iy).L>uKO5KvHzw̓>f_6^Ai,~wY!|h/޽5<(9w߃8wYhۆ!ilck~gsmvsiyPbVA!nos0=VoeHwBU=ZoIu)u[1v4C8 c%/!C( =Hsqw3k2lj(4ÇP*@|T>@|Tw:!-cNlhL RAՙnt%l#[qsW?IԜX{C{VnªSxj5 +fG[w4Yx?}>N9~0TfԦ?x {zvegX;ߋ,* w2 yſLYqAyl\u [C~%FK3F~8h3~FLos wDwƯ+!ʇhdcCSV+E#H1yQA.XfET ,z9glWo a-<ÙY I(;Ө3 ;>ܛmCYk&Zg1ݸOʎ)G̟: SVgjl-t 3sr`W0n\))"-of±¥#+_wuJ,x;HcvƔXqk?*yo m#S_SuN=oUm|8[B>+9{I9?')Je:887܇"ACM-EN6K>Ҷ~}$Ȅys_hʻDMtJ10yBv[B+;>%=qQ2[Mօ=;&,kd f(£zӵٳWU % zS; ]\?YUpP-{ ýF4fgZk֪cFK3v# $hv|b!ıy)1Z&oK]g%XI+2[䮴Y}2gRKC$_I1yx$ݽ׭cYu\2,ԙ@>uδ1^qcVOK1P@mZ-=MHgf6G!e5eU֞Q݂p:yjA:3!v&~s(cd'B=QӮVzxd6Y d^ Z >ne-XX1$blڿHPI깘R m9}15BLy9e^~>4?OZ]mTHe6e([ #2^eoDu2E!K)њjuL.&X&JiMqvyn5ÕqIB݉; $/YE6F kmnd1bF) GT.ގ}u1Lu^hMO(:(۸x䙑 !Xק<{bv4`-ɏi0e:ԣ-r[Cݰ@DP(D tlӼkf,jjc|+AhO﫻/aZWk 24 cnG`%i{nC3}ty2ZV@9.9Ӧ^\1szTj[kR'9!Ry.W9nF0 1t "^gbXvGec/v pF'_]iɂETw[ZWH"G҆TVX-Vx<9mv؛XC @vmJeN|ھ}DF\B"D#B$W[av2"*T\x䌢{22?A2LlɊABn; x"H@wo%zSӷmx?GД͆]TWI۶s%!"!ӔXDU 4"z9|'G_ٱG'%9MeO1.g=Fc%!<ۯ(%]`nWͩ|YcTsq3K<ڤ+|)&_>w6Kmw=(tqRBܶtY||e)LiSRo9u~kfJF7x7thsB)Nq4inKW5|ز?ˈn|;`ZΊbl9fZ|gM_gR܋6]}S0l'Tn-YP aF(NI5Dvtij9?&x`]zjZ" ͹CV9>?C6 ^~7W!Fxl-beJ8<`Z}qt$ݵ#{.xgeTyIVa'>9M.9+>6U5}:ehThCe^]W\SE3U~:ك27*qBtUFE&x~8]J#];g;LZ>/qT VZkYi=hND{)8|28T_;铗%qoMv'/=wƖfd*ϟ nd5=k%㧈$fDض&1?xaYL/ eW]c{Sֶ !&~c+ތRWٷjūoCjUV^F۵/̝ñmI"a2%=ˬvMv7۱dWs;2C=+ !یYҟ? N]5Grᯀŵ e{?pGK"ںז}}oJ"_ZBkw p. 5^{>#@=ӳM,{`+q|vy@j:6dFpt?w=O !?>V kvlC> n'"ƺrްq"]V# A`7^y'x'Yxp,4eH6 )q'VH/< 3yv[azΞU*"*YUnF 'eB4h:8=-Xyr?_xg=VYn%{xJe\) `Xԩk`ݏvx o<v[yI,{âA5OkHo`L5hHoX[@PÐ|b6柽ݹY{^SxYHJ8l0'c6YHTr/1)ac͑j4`0qZ-BIr){c<MٞTF x 0 >'! i $o`1۠4hR&M]g9x0 CD _$e$B6D|Cqn%UM= -OcŘ0cvi,zp''˵XE8Ef 4vhg`L5hΆ](r p4_O!׿8f-jJv4ٞv4c™5`yv ХPX骙g\Qh5As@XAs@ٟ}3J۽bSh@̨h೉dE?*iY?,d'֡ g{bT06yX =Y n/aژo ꙝI]J>gNi 戞_y3F|Lԉx0+/P*@|T>|g㣨>~ffkM% @HB 5PB]PPc# RE{B$:3̆Y@s9wgN@ǙbtA@bB[1` 1*ȈifL1d k/ߨa+j΢F &ɿlyϹO'Иs5$W[9IV>wfj1VT$xF^{#C<߭Jz+N[l<˚ Ғz`]a/Uwg/&YW\KMq9on˭U~B ]tS3NM+k7wVLj6Uid]/гXʇ! }qr}oT@2 ݢ Se#Hȴ?PQak7 \5UV]:%fn@w7SٞU3vX&Y ezz^:V~(@oKC' pJ>PD?3DfON_l:O \"-;ħPa =ULʧ8ƵgCy^zS;-Fkv?g"g<;f'<{i5GX>l.QGyJeD΢l7qkOݚeA_R>@ubdC 3s &Ѓ,b@@ƝIAO=M"PǪo8YTx>r s`ݎD<*?О*!gϻmh0`2`QU)oGAW #d8[5ugS0钜`X!w89-hm:>3ҿxù9){Q'H} WbZ}a^g8i`x_LN;+SZȆew$jH@_SV\-&AI\1 !dygxSw nmʌ{ԴP, p5[8 B.33ʢ̑zvIR}bJ]OO[zkuN'[8wS2 ;w*6xʿ>ݚdٮ;W»)rΞL0 9a]Fm*+ގv38#eW-mp[ S!$(04PZn]RRXvDXI<K 8*-6`j3").bZLٟ>єyZ/ױUU1(ĭ0RU+:'!v%%]mDuϩѾ"ҭȾkW2+U Fw'u|GMjfL&)"k*UF:\!ne2c̒eFUWaa v%K([$KaJhlNsU օʂrqrrb1jزx, tp̏-0C[tvuJumRj.h5:2@*c#) ,o(i¦4΋pJ3=@Ie+ֹ,tcͤKoR$Fc5۾ʼn+c˲$y{Yj4M¡5s|]0 t͆gZr?>Rl7dlDkR.Żر*guIAKNWdaDII޳YΤ3ޞhUee{8fMy/y̝+zXH{nly3 % R{Ȁ!ߩ̘2<[V]pZ{8̍k*p򉄵ֹN8GM߱ptn&]eD,@j.~7}T$' /([LjN.ub:X.0jۑ5@L+v Mg<=&F:66d)HM VZ-|VZ7rz+8:m׀}g*3ـHʬ=V7 s<>+A^ePQաW_)I9攟ڒ.̕ܛfўS'",3@ʂ{tv l;!q_H2IŶYzS^̌y|w;~B)=qڸ&mQVKѓ&l.N,׳`Rj%#'SOj9d\J.0$S$:BJqU-:jn]|Zz\5h|Z'F n=k`4E¾鉅:ֆu.ګXc{f@gH>hRJk93R^{2q):wyj>OXllهH͉W2Wl}HtWMKyMRiP6\Rp$={9_wwA:vRf~&k&LϜ F;*ku2׳"qx};ҳK׊$=F-,ZeOf9[oZ;]ȈiC\ /_/Z9MJ2m€A;=0 ډ统|\.G75N LiAIΐn[8C}Γ~44c'~'XuW38j~χ Ͱ-??IE@Mmq?v"57R_Tc՚+ xU>|Ayʇ `CA| AA! V>Aʇ  XA+ `CA| AA! XA+ `w_>Xld^\; C(g< MPo|vy_U $gAp 1}i\jzxŏtEN8G"+´Ay*Y3vm}J-n)hz:J9 T>eFrlv*ɦ*|ͳ|lA[ %Ճ!Y щbB5q=EaK4"3c +g@r{ ig`K,$T;8}@tI P\Bo귃T-%D*u嶥. jX0Q-L |-돓:;,Dc!/oBh`('V!|u@i!vd`%\6 R. *1 4,xB[(\J`}L?'Gz*)JI(M )cD*@KKho-Q- ( XG-QIZʂ9 T&q9ZF z9nf}cY|P*CBӚ jSzXT^)X(YgifL&KUߍ%!jB ` XjԷ?;y¨ =J@31AgͶ2Bi68<1ko&o^JZYEulp5 30 ؝g |yk'`}L;)P 3|X=2$LX1XډH,7haj- g$}9 U>`]11Rr%Wg~Qm @><;ȇ݈z5 Oy5t̻$Lwg. Ӭ>5FO59^)4t1x=MFgkۗ w KbW4I?j fUa\#fk37h?P b!'\୐Δ>Z޵3oѼ4AD`_͐|ʿTVq N*SBJWeӃcf[IP>^A>v;A~PAyʇ `CA| AA! V>Aʇ  XA+ `CA| AA! XA+ H; D 9I+|2d}ozsaDz˧ ZmpMLpNT+\J}7ot#ÝV<<oO ®אM(x@@?ڵO%yCGL!ϼ+?;$9L:r)}o^f.@S!FnU-U]]_ONaq}꣟ֻFӊoay,sŞ`8ڥskx:͋ŗLӵV|Ax9igH$|`;DŽQsמ<݉58_K;gWF}/q:g~v6譹V-dnEyZԿQC 1f ՙwluf;3[ en|ϑ˖`/'b)*?! 5go;F]cjQ֭8Mɐ_x&)>ӏN-%LʂS7dv[:gu{~XK vAcY01B %bҼCB\;rL|O$ٕc`)4&%pX߄0TjKkZł(:4'yݼQmm: 4OgvfONhJ eZ `3Ö7Uά*Oue7UT=8sD L di[>?ڳi횜}+h91ePOyB-<3Co}''?{{?R ǙR(Աu-(:+clҐOԤ^ۍ2C'^zv;?^1^?sO h%21V}kWky궯zr17IpkrS@8I?bgۯ~XY߾YQM4sso 撱kSՐ%h3##859jdQ +L(.ye/2|̎K+7qkt3˹v2/q<3򼑷8J (ݵ _, YRo|9G\-W=- ^_[D8}cLBsϿa)TUIF8 U*|}y{w|HntR|ԟŚy\]`cs\vץ|{R;qX  ;%Sve!ީx~32Rfջ\[Q3Vқ*|}kH=w]1s׮ggKMK՝Zֺ!7߇ww'Rb_?"' s'KRx)Jkq]kyuDς6#.:KZ'NeRoqS80 uprKxIg/Mzbp${qCOD4R>q.n{>nܓPefN)"¸>Ӓ ޳ ܳKu||9c\{u|y^6;pFWU)-WU( qB WCL8]!"A`d7&jNK `/oj{ .ҢqcŷܗƢ ߔLj֪4x` #p &B= 3Lˋq >@W°bBwɯ|l/x9瀵bT`2jBu 9L `)2,m059U3|uE2u!TkH{͇~S( IDATnnZBYŻ<;l7wCBSgt ꞭcZ֠32{񄃓Ji% &;4CR\5ywfЅWvR5u7[躏[qCLQ麛zQ.2㝏' wVάʎKwSdCekr9 φz8fX!HwC#S^޻!.fN 7[v9` Ų,C ð V|qn{!uwyRJZ y9z$Vӛk pXJϝVR f fj{|_{eP'MV'Kа@88pW;Τ {I jOW7TJnC럴5Z2yĭ \F:7~Īk Wjػ!,nS|-rLJmQ˻YZ/IIj8RW)nڙLɬ.]k1y&3Z{g1Lm_xsU9wc1l藫:i3+sXKZ![3!'LCMBYrfggޘZ8/FLUzEg?ATD)s O}a-l~wy}Ȍwc':gW-;?¯?8̝RF]tuV[oM5iN#.X,suֹ?~KeϺMd~oN]RUl޹H#/y}3bMVδd]KDUF kr:YKjԸuM}b7pf2ZlYq'(;nB[^(v6 @xI`I+{Ss8p{$K*Ftvѿr%_QvNi%m pKJEe fäjLh&9aAp?;rDyKV+%8p0 }U5Cnn' 1abh>Z@ 2e:[;0n'  §=,b7y4T}4#5 Y<Om@q|io[>Z8l- h] vIJ#-!|m@kآq$xdcrϷC.3AvW^^_Y ǃ&h]h5 {=#Ά;Gv4hΎPYVE?ᰡKQ?"'%l!bOO"~g Md ~{o{ .?Yzb"δL,Cp!5Ѿovk屚FJ(k7等&VpHO~ƼbN`skL?Zq76 . 9&ksfeW:nZZbo$+G-:˼F;X|Mqe;4 3Stm^_P 2&8=qHnrC_}m?mw pmqw= j_Z+JRwð$W7y&٠5ƲK4GOvj]`Ag.;u.;<ԩ-_n yZ$-Sks3 Pڦ-uҸMqzd=<ޖ-Ńcۨ#xpΊI-&xk$qO-d}v;ۗ؍-vrb UZcٵIk MH!Jm+6u5RBiCw#pzd-|qŵƱMW屒BOZM&Hjdmb-HvZ *vSώ{Kؐ$ A&ekYp`iD{2ɦ߫NS-[aiG7EtmzK5Z!_#{ޥCo/4uMS3 ܲM6uM⭑ѽ V~Φ]~b7u[6E )$%*Y/B֫xe;MHF =+F=1-KVwW8/:$A#C Y~h\q WMno$=j\jP _iN + B?LezEg?IVR@sm~ϾÂ;{E%B;Z$*z %)jKn5n(=jwW&=mz[L-\<'q9X<\OV:[M⯑D ʇ|Ae!O}~{?^QKѸؼ/{;CP!u:}G686M$E;G586ۉ ? a"X&mzs~[F΂}hE>oƿr- VT`84> p gޞF±V|g47Roר^TG<-z;H8_b}jVdGŐ^^-ffAI 鼷Wu&\?!dNGӾ_.0 -|هTGo4p4S7~N`ԗNt%>1xoIM{7@CQQ}GhKIwrZ>ߛ;og075>R׾+Y'ʉU%2Rttջofq Nʤb/Ӵ#=IVuSVS;9}bۅfoIK~WMӳ{MA}O~ۯ{O>7g>5J8Z]=<$2 HK|mںIGv^4_ -ɾJ#vBw±\jTu1^[Z=8 _ilA:qm:@Jk"#Iߩ"9:~πXcX]Kou]{Sݧ׍NM|J㏰7х4`nJ}&R oW"XsUeNUіĵ>h&_Mְ fpLn`J~ 1 HU79 s&zK՚OlrƒWo8r[}ߺy&r @դu`YYaaX20V4>,=*3Ǩ7_.i+ C3 ,$ uu5̞w4}AL+z |E$>[Yab;1P['˻|4-'q9*f! 7 #SS70_c]4>&cUC=b@Xٸ9}\&B \F괷V-rBA-fjRbطuZO y@c: f,7ͽP~a\YX9Pm8&-!pao[H%WiЁ^"B04ʿ0)5>p^7!2 HUջ/f󟴨X,eZgRL ,\˛swG~:xiٴח uoz9Hڍ,m-LcCk*!8qݟbت~cقU'xk9S^Oy;~>q=E` _*h ّ/nGi5 AyNAw;A+ AA! V>Aʇ  XA+ `CA| AA! V>Aʇ   N@>P\\N5 `EL_bśᄋ@wyp7?I !S^nh ЭSf)Svfs.VPnxM[:%?[ ezz^:VHOs߱!6ߥ;DO$0&}0~fVOg̞<tv%s-"-;ħPa =ULʧ8ƵgCy^$Dk1 |pنg+ə{ \3nPݝoXB[@c a֔\ݳTedifoőSWG%l΁×6g5o~TF$oW7 4mL̏?JkC[fe-zMo3 n.쮽ۦνg>@Ɲ w.nK [מ*3AWb-Ku]T JE(hnou;uvo)etٝΦY~qp߷-@yi:Lø`?`lLw +K:@=Ei!E;nԹ\mwٶ"H} WbZ}a^g8 krTNM<^T̀(k`mکjT lzf5!eAǰO=uQȰ1޹YW`z{?\oIȻ l6_G]eO? +/n\uY1doM#IOlܓ[i2cKWoMbjkZ{7 NXP5e1qJ*NfuJM"bD̀% n 0^V7MZrPYP!^N>Nz>C^]Xl`#) R$F4 ݚ8؉jM]5*g1I'9tʟE4^foQ)uMF }gkb}V eI2h4Ck4~G2'MB(vMk&1];W1$癤7iQfٕyNaLǺt=١{:s?HmЗ{qC=ߢwn ̭0DuڑT2rggV y-*fŎF}nͽNcflҨQ./"D:[v%Ԯ??ܠ0[Asg[\9btJvo`D1+O:#־ȣs^l 8#>rpWȽ@yi2c͑"!zyʆY7ˡ'bٛih曆\>ug<.4CmKԶUW-Ofq͇ 5*~ˏ򯹾-K>+U2mwRA;EpAv\V_9Sd*`̋"gմo zN^n' l9NNwfZⳝ v.bQ;YّU]Q 9mftAolS)jySDXy?S {AVbB,*zA| <*?О* ZMw$$ 4A|yQzd~8\upSfg?rR_5 gȨE<7ʟBw &NK~ IDATƑoAG : T)B-w;A+r'!K_ԉzH['=>7Y9O ``iJ,C[C<߭Jz >f ѺM=EO -:aDgC\AHEN ty/zZHǐ3Μ3o6?LB0˯z&g8ݻ(!a9VF=Pf@:i7 Q=jKX[tY6C\$bK?Ds)14Ƽ+!9~{R*MZ!u G <{>[St:{H]z|`ey%5z3mֳ=z/%[0\=k>U):ӊ[7YyQ3\-SgSo8Wz`#ןw79~\RNM<^T̀(k`mکjʋW]V YN5nRy7O1oa.oD5w&!*ܲ|%w4i0]RWLU۝!d!cОٔv`nӭIfFyvEx7EٓT$'Gfu{6U[ S!$(04PZn]RRXvDXI<K 8*3-ٗXbb1DŢWq5;us%fi}mUʈ!cL&vYlx݌-b"cdcA} 崭Wvq?Y)J;Wo],(/''g=a!w.,6q|UY?)ՖVuk4IHH$bFStU|usAxؚmb!V>pj4M¡5s7ʽH]\ݐn[X Sţx;vRV.)(sI\r,"(>P ]̙,0lk:XMy3 % C**Z*n帩 H4.x0z^pߊS2tn#:AO9wO㨉$ǭtIx̠`uZguSq*O.'++OЫ&{LJܺ~fk;.̕*ZP^̌Zld?~@Ap͇_4vA); ."P[zf_wؔޚkW?oK4Ǜ)}5'Y%SåXfON_dߑ:0EŬ1;ߨ/ЍIPR3+U{ֵ<r"ؤQ].۾Eh76\J}7 RG^r1  |og{Mߞsa+>I_f~iҫ3ۖEG"|og˹{;a{1ߝV.Z˓86H o 4ɗrDw=K1ş"_ʃǣycVR!ˠJy߇8do\blbĥ(@6ئ}iv5[@5%{ $b^2# y5o~TF$o:qcSOu$e+ƱN0咎!bz8 GwLd|Y"'/s]\?x#|E⋵fڢ+EuDžc'2C{N"-;ħPa =U`nzݒϻjX._c?4g }/ߘ{w ]OYDQv|qef.; 6^`V %őSWG%l΁u;iS Jz5MaO蘵c_Z.)v=Q\OS`K2I0?-b@:c VB#_q,8}J. =OfM=Nq*f.;-Fkv?g"g<;f'2;F6$I h|HåĔQ,>_7lƇSӧ,.14).Li-Hic=2XXzoO.*et qTE:/GJ_5ljzq8uV>\!!5}bH>#N5cxɷvRZjtdT &`ahaeX$Io̊Xh°uGM֌gʓOsL?h koj:Zj'!\v3=@Ie+gINWk ^S8H@ ho] ?LjxEo!8$ f͂ IZ-|Gw(t]xl*[ M\6HV5+#y2ٜ:E!eIκ-̹X20Ba:=e/2;F7׃}) Z\~q' I]wd𠎆mEMHH@doG N¹qޙn)3T'uTRgDB6k<0l(t¸?ԬMp)Y9޳ :s`HVg6'<+ &,04(e'PAtf9cpRNT)]|oYgdͱcC+r,iWޙBgvs֪/=#v}Yqh;+n7*j+;nƑK'O螷yZX GɪfE\"̄ <^`C##_Z>Əϫo<YEEx7^{n?!AE|uM{f_N,ױsy@Ap͇p""*U";ccK]Yґ@F?9OL}b[Lܻ|[4A+½8*(Pᖛ!=c8 ̿{[-yG_\7ٙCmO66@k|W v>bp)4B֥{`c F,䬛gfF4V x"2iABJ=oeR׸°&mQ֐i z8F U{8@AZ| TEszG717R+WEAp͇-DU{+&Y]yґi=X Z{;ylk'iS-ۉ[(}:V&$QfkƑQ^!bpۻ(RED,{C11&QcKL.ɕz~K$b{*wX:l;c(Kݝw3ﻳEu7CdBG;킣G;{G;BvX<[/R:u *X]͜OϨ1t1e{1G2 [<"|h}va?4Jc;gl^+_OnS66Jά3Ӟ5x#AҸGɓ L÷дIj=ג[=c|IKDW lª/nR%b ΋Tݸft4*]?X@NI'I/SSCq{rFjs[Qu9Vw*P$ֹ=IY/2{p e4I `C+B@wLti^oQo"> NjI]1l uuuu~^R'崑|4*r ѓ5G2_us 󇻟o)2͝P/''g! tT˺]:hBu,'cld:sXfVTXҨd+m2B$ikV6 $V[H_]ǣH.K F<ޱ!hog$i1(B(XHuzS 깸}t1fѡ._Tg+>1~65G8u~Dўw/:7w1H-U.Fj/&#‡ .omL;ˉR__| 8N - W`"s݌hVqH,t.Niӓ'Jo3&87gjozRǖF ˄uj=]/iEM S5NaVvuR]ck_) 3 S=Cnjέw{8J=yJfFH<;iђn)qO|{]A_]!8,]|ng=BÚt ޽&>O3~ޖq") U`@X CJçtJQߞ9˩.#ԞC՞l/vG;퀣G;{SBvN`J  <]vag˿]`IѣX5|'3z<~]T>Dv0{~u(fu0Ϩ߷g<#.Nd_gbҵV$9e8'*-6%NszY9aQatV>fc#'S (k|3B`QatV>`4!׆aLwΠa<`Qw.A0Ϩl_8atV>^Κ: ŔkccLQgdݙyF]obl]4PmKȾ#pАZmaεca3"!Ĵp i';ثJU~@ 52XZiPsEţ e;_WzQ71b,9m_!qn|߹pi&ґߴ%Qs_)Z~P ;7YE+FGj+|9xb`{3V"X3%\#֜ssO\L#ʴ,@+n-Z0gkx!|q[t&{-o-ݺ3]csnJ=S{f3W )ٳgt6JMH?j| ɷ%>>E PMgǰq躋x^x/vG{ֳ'Es;{S$0^at@!0hжM]4`8iD=8sRBY vC IDAT"0>8f5a9sƋ_8cڴ)#M6iGMT*9 @zL{X㍬a3zzgu3bn^9*v"{syo{?^T/u0Ϩg| !G"G(ʤJ)Ce&EeCg o%S5iўE_lJwa>f{e5Eħ/?hP <#=߃];yFaC) *TZ$v,o2 ǯ_:㟳L~qK'n|A=ɧT0%ښ 3}Ywfo*x0QF%Y,,2,pw"I Jk4RP^9)l]+u+.3j+W9j_(V`+Z%" ,!2$D#Hb2Y,H @,$:=өZ;SRQ=z+t'R~?=fA!Gwr-8d0"}1n=n? `#Dj:9S+X^P'oViGMt(>\iq)F^{0,=KWEP)/klc̼Ef͘2אydZ 5aqǎ2W6fDkdOo9z(QfҨD-Z`$_UzVN5apWԱW"Jb':߬'y$zݛ#MSfȌ <׷o\6եFFAP=_|Ns;Ey-"??j裏jV>r=yF?sN@`o1]_Ze6Ěg5zgW>֐eVe 9zԛG+ŤE\p@R aуiόG|WĐsevNЃ=!P;xo]0=4g4p.{;{S)E>s^ӧNjQ;ah(+|8ډ)E_{ïr׬h{0{;=O)-&]kEґA[f~kS41gIњE,!μƫ{\ӈyFgyF] }Rd픢{&<1S }FRO(hKrtT]k uq @1t0Ϩ; yF}gEX#S=`4!׆aLm29( u;YS*0Ϩl_8atV>ݔ"箏{z9k4S1Gc]ktg*V}0#pАZmaεca3"󡁇jH=ssM'zLAY_[7gS ʍ@P {p7΅K5yWqIZSsF pJxXJ?p Cs,4DUڹQӝ/oԍknu^bߘl1ض/[ϰF\iKNSʡk+v\o0WčR;VXs6.)`"r+fK噯f2'e@L`{֜ssO\L#ʴ,@+n-Z0gkx!|q[t&{-o-ݺ3]cs"cE-o]e+ђפ& ٳGh&$z5>[QC~D&}g3cظqtr<= )L)P0^atQ7O"4~g`Q3袇rvNЃ=!V>?9>O<wtV>BXBAD}{ie$.K6 s#Z;Zo`̵20gfВ;/Vq 4a&]u&{F5"f)\9&esdx1hBpH͞ % & +4A&Ȏ}z0q#|EKnVjn܊ˇt&5L 2dfG?2b XP=wLvB$(9D,)fRP+_˵!=3uΠ3fUeBXPH ZZG(E S s ǭS΁͵ UB+6>_/DК, YjݝsmBd4֒tkś.YKmV>zϗ}>C%Mi7~>UPw@?$j[iJ:~OCq5csUOMnRې`GyNcMLQ䱰M#*w%'Ί s&7aCJS$٭H Z^|l7%7l=()D98.ڂSG 7%,uSAc4 B>.x>~sNa4 Bu] Th!\ s.v@ KzlBC.ވw,P/B|!:=.:TBXZ#WX<.MBXKimBX J)W;q"PggG,4뼦/KɭJ+U}.=:00op]#coݕj@Hlpvul-/Q`C!|,bp! G4! lWOWBaC!$6@}cEl 7r~̀Bh'B!|!& !LB!|& !VAW0Q! &(B٥S0Q!tL)9L)nLB{&x0Q! ʇB!o>LB!|& !Ѓ>b<<(BXLBG;BaC!.& !LB!|& !VAW0Q! &(B>L)K[J& !nB?ct& !VAV0Q! ҇B!oD!'sl`Ba\0Q! &2 !!V>B+B!!ʇBaC!!BXB!|!V>B+B!!ʇB+B!!ʇB 7sc4[#=Bw63ٝB?c\@H914q _\B|;Ecϑ&Gj8$D!1v-cXFB:toiIg7Bi t窊dYJs-B?W>s#eqf %qWW'B(<"hkE(?x0`S#Ǖ7?'2꒯5Yov ;h^^AwąkV5 3MB?W>M5ݏDq![LVxJWSb{}1ARfaS#ߕvRJ/-8ޏǀ!H#̯2zB6n\DV5X@+5i D!aUm" ;f։_kT隭L_&h&hc"6,B~H$ ! ''e*{}QcE!J!TLBXB!|!V>B+B!!ʇBaC!!BXB!|!V>B+B!|!V>Bhb?Kw`4B|!`|ůf /ޮ^O"fP` ]lwDg}~ӝі S#ktpykf'@G+ 1>z;AI;֎XX^f(;y:!J\^$qfuH>o=Ghu7o3Mс"āu{oʙ)86~464;8ݩ|TB=2kninҌsQQnA/:$*Rx#W4P#Z[rwJbcR7Zƺ:F΢/^߮tulo4SC]ٺ}^gvV𪿯n{p/;|?k_yUq!3Z(2`_7ydee'L%p'%sFoSmU B`7ˤ)?}aϕVUNxfU[ysc:XK-%[ϼwF+ _&a%cVr7&R?wrlyjfѿ2uIks>9Nߨ59Dz oo@bg>9?62 ]~.]lYof!^lP"Wh v\$tq]apu66O*8~0DdW.K#ח{ a"׿b?ߥڬ-r>NcͭY׳ޮM-`+4I7ᐴIT.3١6ԅ٤,zVx,tv;SCX,VōLEfteAo$B]kHgmF<cc+R~$9|tEG wIyOt:-!$ä. xE/]1'ׁ JtھI4 ϰX(͛.eg~u1gޫ*QѤDSH)k巪˾ )ˑ@~% wё_T(`| 4-@|tS}@]RxJҢT(ISNjidT8yqa""ܦ?zH}ZXßx{3bOIڕXTٕFKД.zxonr^Lu^W=XJfGdV7[<#' 6ipbљYz3px)OcDHm1d=浾1P'9E;`)e2w*(n-OiT:X@P@B(ٙӐӾDb\N%ط9صOݞh>j1Q?3lR@X;[i$\x[+@@+;{̝EiT:@!t;+ܽ\kdf |uUfMe5/W^DQ$EdG6lgXICEYk"8j -z-cDL=F5iJ#vU/]DA E}kC5<ʺ_RmCzs+)M}=?>fR#&]j5-*ݺuf/u!6O9=WE|Sf t mJR;3q/nk& 8ZSMQHKjs @ŭtKC?B>nx7Ÿ/1/'5z#)&97>h@!r9ckQvq7Fl70b$O!2mR`MV^[jovr Xxȼ^3kO>lбand7t tm,i<9jlW<, uNv1 i Q<TQ)}yjhGޚ'_)6)*hK~AP^PVaҾkϭzZ[ie |?=6' +*j jY NR"YJs4n]#:q,뗏4\\CHmly۩I!Ξuu mv]?:AeJݥgqش*Omy.*=v8t_Hն>b|O w"(9.*[n<}Υeˣϝ;-o.>kYf>괣'&<ߢj*Uj0K6ZYg>ZJ}Om#%W 6j*l?{~sٞ?uisη?@%mWZInGog[BQB>n\_$3n/IDAT#ߞ~v?\-QGG~Rn!XB.QEH$Gw깝ǝӾS s; I/pr[_z?:K@8l|st5zDK㩒m s OepѠ[~-WCG86T( CjV:Zq]AV=⧺)\Lkū\4h*o/n̺ߟ?ң!P!\!ʇBaC!!BXB!|!V>B+B!!ʇBaC!!BXB v˼BdC!a+"/?m}zFQ^_]VHOJn|!R~eճ-cSj}Y{,+g.9e2Ta2#1G2 [- ǟg}T*6B_n[ghdmڬ_?bxhҸ OI|t{>LJ]|,oёR3<꒠tSoGb̏)]KmÇӻZ>_.B SfK/`;/v({Wu3ƺ&6SxĢ+Ndzurp?vi/tR޽2/tX]7R"PGᯑ~@"f 4We~FaC}ףq pθ'.Mg寄Rv9LިկM4ksYcU;P#JONTZ5t9_^_2#L'L?]0=\7m͊%{w^2<6S &r~!kcE,!μƫ{\5̕ya~g)p'/7~T6)*O:[S.(6֦ZǛ[y!]1krj:Q=:屢;Ã'v^$n&=&?&ڍ/^~mL߳| a5IΛbC"mK,ߐ@]5eTKmYpz9Y#>QݓƬ\ZZ+\2QJ{:B=F& Ro{Ҝ 5^kiWU&uj$xޓW-.*=4] -ċ[7Ǎ6'j*ꦄ9K[DI$v,o2 ǯ_:㟳LC-! E5umʆ+~&7rr3@CjJYܤ`&w5fyJn(tG)pR4ZeKE]%3fUmInk-.$:Hاy=Ė#[flg%L5&L }yZoʢ"w'$ByŧқLT&UŌ ;эyIQM厥DҝaV{<>2;]%QFmɛTXB,s YK(BQM=9^IK6sg9p,z\ \uWdj;kRDTK/gW7]Syf^_D['p3X!/TjkFVs9IK[1v2&0,Rw;jڠ7\.}#\.m0~ikORhEXh=:FjuOu+l}v&8\v?R__| 8N - W`]$7-\0lUϺKԪ:';*@@8G-Z]]2 e@zom_߱HeY"BLkdcc0-RW۽CdZ.7qv qn$Զ됓RǖF hpaY'jk^1'[5#$D[%egH?TkQ4( !9@5N#]Ja,cHomC5"VPvv4 մ;`]lwKMq`H@fy銋?1>>6/>}}ҙKRrttMC#ͿszyMޥ#9WG-dĶ}ݨ ڼKWF-_ȖʺZ ~>O\L#ʴ;KnJXYG1aґO,"lTzoMa({p ݐz6OVu2 /O]pzm0SwL {ϗX=_#%8;\D[q5moN;n(Nɞ={ˍ+w~դk,cظqtr<GH$l^D GKھG>ʷ]BpYlGrV>|1"f>|yV_aG;B@՞l6$z!!BXP<݃IM/b{Ka[gc\\W̚W~{-3!An79/fO7#OE|;&"|袇A?zz1_NTZlΈ9oᾮ"R[w2=αϾ|6dz|3l_y by:v|xk+ L^ &؍gQd9x2Ʃۥ֦E)K[ /:h71NMir./q?uiCw^e ~բU?$}kOqn] F.X>_E^9&G 6}P?3I?t|XWznu Q;vX~s^\KJFc_wtwp~(ߤK9k ,=~Ƥ;] KMk܈wdqFҫ'K,!)&JJe1͝D;+$-XAo0HD(/ͫQTB\IPX,fkb ?Rڭ Ue|+7Q|ppSև 0&a78<4 j[9%4mO1j I=u^#8vlԲ$bCWM[:uUqO>ͥnsROf0prmGTҩkJXep[@\/Mm746sM&{T;e g,9Q_ʻ5ͅlQI#gr傡.|(!Pw[?\RQrjɝvfXk-N.3j9_(7SƆԔ2>RWd兕i3iIM:3XEEw'ҚԓdtIiUNlWd.qRKrkW:9낃9-6߮/JNo4QzYRZ4xqf6滫Cf <} @.p(M?l3plL)}CQ9rݛ )?q?d0CvFu%e6sǎBLrnm;Gw_thCRb>`g=Chh;%Y$mEp **Tf;ξPk475;ﴑ~6N4Zk4jO0}q&Z葵l00Z |m0U{v7(b(me}&?).=S6:XM}7 iDٚNuL0o#CBX<>Cb!c 2[NFϗH /Z椞vsvB HUc]ENk3bEXQw 5 ִccxy' o6M pTC3w7JBPGIr{1v })+aF}A'tt-ga>x770'v~Hu1> E%궅"GG̎3""<k8lfY$!2rIfL Y'jƒHuAl9™=|ZIQj2[۷=3җ:D & 3/eL>8$}:e0Ψ␘v(ÃNg݃k˖>L}ujAN7)޷[f-qNN)N,ߵgD8EVndwZ% :g ꜓N3L5XjOx!ԫ,'ߊ2̣foxs17$=Sl`yFrpaa׌4y.˩{ݾC1P?=McVC¬~浈xSw{GӽwΌ8}PA=c!f5j@2/HZ.SkDw}ɻX>Oώu ) U"3~ޖaFa|O>SFwU(VЏrnQ(&-l"zvD]qIENDB`termimad-0.29.4/doc/table-in-50.png000064400000000000000000000570171046102023000147240ustar 00000000000000PNG  IHDR_isBITOtEXtSoftwareShutterc IDATxg\W ,KHSQQPc5c&ƨ&k$&whhlػ ;la6~{g 0s=wvyHt !@v#pbDq@~k޽{ QUaV]^ԍ+G`K\9-, *Smtdف'N|a"!D\X%qTl5<};د;]1Wڻc9ur ^lc1`Bg?W&'CJ`*Bv048 x_KޓXu5;-hJVުYEv_aio~!V҈gN+n/o E0!;J_shw>"nI(w>J!Dv{̺\n t3n;x*JLCZ5ڍ&+"L[]اKkQ INǁ_xgbdkXrW۷\U,Xxv-/19u:sL#[EN/bbjBAC_D!/ $1\V~ ʩx\s2K K/x}m;cT4;XZQ0ṃ!ϹCI^]H&#ԩ!Dౄ&?PuvFQ͛3㝡o[`G xͱ\ߩeߴdʣgESodzٻxvVij\;I>&_hPH@ |,ݝK/^HM/˸+~=bؘ`W5 U$\[wcaW}9"PD䖪e :q^AtaOEɪyv[9BJq1ҴW#|mS2%4"Z&!̭,m6`'JwO*+-ƥ}+;L"Dr:a*2)Kޒ"9z~$>8BH۾)˵"{trpc2 LGrq< te[9\)E!Ϙ{(o]'rl߅z8iniQU؛a'P- dd& p*KhD`/"MחE;7>ṔF_d)y >zZ"WyJXYVr0~T(E1ۉ-~ wP˗\S$9Wn$I"A$! C$piس%o?(&I{8]0Qli}V /V%HI.Oʴok!DGf{q!Ôx<Kn\Pd©:F-`s6)VsVb%;p]ΝPbX#Z?Hpդ%f5}_NiGiy]f-aKG"yJCOr!9 ,`ed`e@v ;Խzp@3P^z57v͓J%,@v@\cϘ/L\%qH̝]\wIK`-rFߜ hk]>dӥgz%.J읽`GU虜1&i-b^D$j^񤴅9ʌ|M$v dGNI6Vh\~\\4sqI!N<}⩑2+աCyVf8Bvj0&.י#m{(zy6gʵNG Bpx!Y=u/׻(.p/qs"3.B\_{q=MEu\ЖI[ggm] Nv9{u^k뱽l;Ygf!$qv,(-gE{ /W:gN ;e;6#h(۳:&|{CMe|?>Wv :SW0~[-n˖'X.j9Rd|8/TZܨw&Ze5L0 U|25WM.G-Gt~PO}DrV!qw<IJ6!MVoo=ɡK?COx4^?*9L߱FњGo UnXY,EŊKQQ' -F!$o"JaL7W^} sꖵM:(nܠ;hTrza!8VU#HIʽyn-gJrRk%9{nDQȌio:B 2tQ^?l)n#ucEU{ZhsBz<:lo˚m엇x&A0A\7.R*RF,{_5,JVNv|0^ʳ1"2[q 8\T©sw*݅gU,|S狿3g- i܂K6 HdkQk[k ςUB4Mz+kylzف*RRO(˭W5%t{ݫL\Pll!V<:W\'A?k(FyJ\N/PSdNKbbJ2GQ{T P젎=>.ahYxcF qnL8HUaU&hȡnɼ)ɟnC`cMjk2hf|`gBz8Y?M tظwX^gT4LtQN3v&o>N5_YМh?&jf#@١Բ ;(/hIP)O4R ;Z#x:Vm &x9V?U[@Y b+T1ͬi85@vhTRFc5*W]5@vhڹAGiPéP:TVc5X5@дQ.-?*\nwGJpP;d ;4.pp@3u.c @vd ;!n[g-8]{ݻw߀|m{ƕ#[\򟳭1̗κM~9sa}+Lw hXNFΆ{me^Mi4jZ #*O:)5` txSbF RGpM6*͌?u(1"L[]اKkQSN ^L/N312!ҪC8ũvVfqx5!qmC!M8M}\!-ù ;/s"ѽE)qWFq$b4Ӟ Qb+&1v{!S38Q\Gyg Z+v[C,u<`!ıEw$ܾ=:ϵ ?mLH#i^VyG.8:U'SNLy qZ픲O`oFS;PY7-|ӿY;.^ÑH>{mE/u>]k5ii.:!p&,M0% W8.B!ADZ}3廣Le_Ұ6ݺ:!oؾDu=]" R-[iM De_7oΜw޲'Lyẉg+- Zi2ml9B7{Ow#*;-[;~!HG/˸+~=bؘ`K.Lr޳loOw=5@T'!86쾑E_;jO%ii{g)iUÇI<S!Go{zr>;{?Hc.Ch4SKKBtov*j.nEȸ]Oѽiz:By DɢܗnA4EQZ~˞ d@71t{!)٘"ei^89H2r7vdZyeL}:D䖪e :q۹Kg0̕]}M+Em҅C3R3 t&CպJq1ҴWXL,z ֍0ane!}oö)S>h(*Nzp?5n*C֧Md=KXJF<4,dh-3e)ui*2)K|0BeٸoewI^(Ynu-Hi&J姦Eb@r!D) Ф&hx<1Pߺ6NWg-5 9~*b" ckeԉo1s=ca+O\z4g4Bzdm+GcLj0I܅+Y^EH[-# (}|54xlSYuY,S,HC!DZZsꢝ&Z-׈W}7-Kn\PdAk*$3խ$˯iس%o?(&I!D$IrH!$I 0af: ԵѝDXvtv?wR[*$ɩQdw'RN#TW'};: ]!AfO؀`ķ cMhItdWG"L|<-W/AJrq~R}[ 1w :): jLp-|q }&QyyVm\NY7Wchy]f-07 `2RLCGb!7t?`<޽q-1-ˌvh$ )6H 5١,ioHYAjXY4a:wˍWG-<3v;qΦ9' |1ـ[YEjHozR+ !D~eW[UّgKT˻^@u,hv V 69n@C Tl\h١ÆM6riN_TL 6 )yv y|C{pΩ'쌤jP@ӫ(5&ݧO81yZ:Ys47Ɠt87WlkMvi߹FSx?בUx{p>g|tn 4ӾfdHX&Vuez؍:~zpAWOFok 4젯w's>44B\KWAѳtjH*Dе7Eʂp $ژ{GYЃ=ch@c=%%L8tŦ ̳7lЎ(VFHW@ oʸAGҼR~٦ !gN/sh:s,w&Ϙ:7%4B=ohrJo/SN6y`'_O*MY8ϱ8uĬzk~;|9c)aDcHM5c^R(f JQRp3;7Jn00iTwnh*١؃ -? ߪ04ӕsaǗ^ǙC^ @i4BZKv鈹v㧍 i;zB|1 &au.Lr޳loOw=5@`$Дt,d.8s%mW_OA2 M:;e\SL$V7VfB|v04BG-)DVdEUBttTGN\ a}\B*h$Pg6+;`&oQY#4Bw0 #Taܕ=HiD:YN_=ty ՒbXb@NO12xBɁ @ՁgcgɁ04ܵ\Ljv%@r-jO× n4$mCݍ `q;a@;yg6&ŽL 0\ƪ {u#&dr?"t7Q[n7OʖTmͬfuxBxT~q݅E⋚Pvq{1#~#Dmz_74Q5#d`W8&ptMfgzpZSOZppk:Oe{09;qҦa=1i;Ĥ%>ZB:+߅/{v3D޵2G)翈I IDATdJR./^F!D=rHWOK8ѿ/Lj_08c˓_V#}c!d︒9~Ev̧caʣ\΢־GB-Lw # B&=HW4BwQ:[g΅UHlyq68jp'*oYc֘`,_ߴ3=Bb\+ 6BۺSo/a9tTWE/5 *䘸88pqm47v5;rZcט.!;EO182 !5$i&jKKԈ5`RՉۓZτ/BTĨB!х/MW!DKÏw2yLHځ9D(JfJZ{B^jjnF| !d,q)R-žtK>:1u:f^?fz@E9$=J_M0 Tx]CWv zځEwvoz3&m{`vO㭱kӡApH=kwIbtsPOS4v3o7Ҕ'qBq5B)s݃BMHûEZ*)4sv~]Z!*7&睮cֺw}rTB G$In-`v"E~gr6&JDMT5è遭q<-K\i.'CL3viuSxlGH2,0j<p:ƾ}©o-͍'P;VYSC7``_nx#3u AvjQ[}qjBHS:̂VU[4Z]N5iB]ynYV0>mRooRΉ'hu ?ǎJtZ&G`e";ei]v!aIv( 0;(U|2ӂKk^ab*e\/4fVs'XHUdK!;rBa'Fk!(z[R Y^}C\G nHoy>c>ܿumLZa_w*Mpc-JL|ei)VCtXGAmHĽߌ#}V KJ J:bP@ګWVj/GXZWnvU9ֶ|4r8|>SRRZYL1udooKhZ<^>p#zz(:|-ShEjͨwДE7㥊 QqӦIuI~ًXt۩+7eܠ@{qģTi^NgSlSV'܍s,r1뽁ޚ_N-Iǡ+6-}oXgoaÇvDsRuD>0oArhzCiRMK.z_c,Zv;z԰ΗNS?' v=&3-ɖfRnt]hMy]XlȄeϯw%i&a{♪IBuY1Z;M 7JViov͔DڴˌRu^LYnرNs>i|Sne"DGO$gۀcga, e3(1nX o¢CW*4ҭ<^<)٘"ei^䃸U]uǶ)YFnL+2= ?ΠsM~!kIw'}Sp*g3{ b֤3skg+UL0"Ĉx5hL!0HC#ĵt=_/9 w~}O{]y"X;|ok>JV"rzO$!aϤ*/_ڎk^!qԗ!|CysYfꑌL20УNe5is}ۨi_^on)lCo6t1n1Mw[xM5ߘ)Mr'y)UZp,w2yLHځ9D(O/m+V .;D%;S Z~3>~s6) }n^wq&N;lێK)ܺ KݍtaAGu;"hp粫?DGfy%aJD1ssO^-F}G9 V}kvܗf! Ö3i TVe~sף]UV*C}yx,œI+pk}|L!㮉idd*jglٽ[+.L"AJNu)-KLh<׭QvaG>*z5odCL1=b^=b:$[1'(1K}02aÉ$%n*=ZIdy7I˻!DHSOޛ>){.$qOr{__z=˺Upܺi NXy47_G%4r:<{цjIFZ1:k:3:vqNufZ~IIr.%gi3)J1'Ϡ.9yx8{#|_؄<]zK?oLnBB\ݾƛ/uuYc%:(&#nYaRؿg Ff~_sWqPp7΢*p\ \C7!pV@vhŜτf[fjmZ^"mX5/١&١WͲC&aY*4/Ф C \]6vջFʑyKE#Xxc1ii8"-| w˞] %4?jPgw R}̹hswwOII+^!z\ !2h>?l=t%mԌ.{3幉N#'?Hu}wnûf_[NS(Dwߛsn\iǢ;'8s+,px'QH y\ ihxv]N8tQ4e)SGu'JԈi쮑L8y !D TAM+iɋ?"5{qJ<%*Abe[AjP<0 q1DtI~ox&H?O󆘖EZX 0>ƉB00YB_ȗJ,e<fK>:1u:f^?]b-.z^YRTf E&DFAv7a?iGđ;DyNɌlU I$"E~gr6&JDMT/^1@sJ+7eܠ@{qģT"-&Cmܴ)㇄xqn|:cn@ʡ7sˊmO, }Dc|*) B"Ԧ{b6B\e сt/^bsY#zs-zņ%fɝ6Nv҇g/=)Rn\.T1?( >_R`h4j{:0'P}x6vaao7\9۾gA*]rҒ!u793qfIoVq^> 6qXxNݧ}q/~PL _qy-LS:6;`l"^J(wMycl'|[nEAIDAT溁޳ѵ;ճ7462ZMߘXO_@snߞKe4mcSum?M3;$f\7ty kvJb{Sr)FL-JLF:%l퀥[YEjHoz~c).Loc)H9tQBX55 %QO 8kStoKgU4t+^WI&QZAXz ´ou2ݵq aiq@^(n<sl{T὾-HIqrzxݼ\%r>LƔ@*1ҙ]!&,w04*Ц^vfL"Z(jt}sMΓK393}WFS]&45÷P;Vd`#:֩:7ʤq^{ѵE+˜s2è@] N ~}4-3~FmQ3-8kխ9ܶ8q.x- kESfYpFuyEQ-vVMQL.f%~|754+aځtĨ' ^v DmʏU嗯T΅_"xgOlupNjoe4ZguqNjv`fكQ򦷝~Tr(iSv>?+{/u\rTƹ\6y3?>2a'zz_s?Tti>wwMKT^x7kP9hn/ =q,%}0gcc7JԌB6[7[;0dlVOO @fJXl 5Z-׈E^ @],t Y.[R"/#Ϫ1)?/9'{/u];Zy !.~pmo7c7d}lUQ?/W?eٖtAIΞZfCt=+4WlLNX.g_^p%s>n?׼5;WlK"ϛ?W=%LDk?8{L|&},Va^|sN>QI]}7ңZz!Ǽe"S?!aJRgI)q1֍,ܠf'aeoVp/3T12_/c tihYq,9kvh:7Imy$9"斤,6j(N|^$H~86EET5lސ6 nee"[kE~~JQP@!$ J[pCsGJ!af# !ۼްh..M (MERڴ`!w 2I|]e+E\ 3k"4qZ CH~.J !DZ[XʤE*(i_QNS-E^6%X`v6-$6JsU}+Hm=[Gl/˺fi tim^z>*Bеw8=,f !ƤX?&JR$Z9+<)s%Bt;r- $iv-hG3B 1CodzٻxvVi45+Ft͟,^{tY-^dŬ:ࣄ'x{?>Ks~KVƳ7n,e|cS'jss P@ 7:emgwꜞun}nGN=jjN;s/jZ_Z/(-E H !HJHH[rOKB~sOy{o }f&pFz㧟|Xkt W}ArnqqZz 2gOw$Lʛg5΁ކڮ -K귞awwTUT))^ۜi5FscZ s35ZWk6pJ jw[WR)/>}wB;qbEg)JF`4jqQ2dߌ41@6异rÀ{aAZtY4y9 wW?t JIVu5Jzn̙yi#WOgڛ:<ńe*CZ"w ^k2_w@eaSg%Frh@"4rsn[ Gμ˾-7wXg\Zu{zc^螗^d~4Si[vŹΓ7ĕ8wRaҜXrÌFy!ɲ I%1)s^=8; [c|M,=u$t(vٔʞlro,qeUW) ^G-0$29?ީ3jMmV>hJsJHSwVQ\b&f&EG] V;t΂3{wݴor2ĕ*CQmw-ǵNIY9Κ?}]Ͼi73CIi.dk༪f7CS,9xXu|5U0J]iM%/)n]y%T*; 7t]GQnoݸ5ؖa: $D@zp#Hv zp#H?6QуA8";@ X;{Fև18X; a y<!4v C#NX o7lX*T33q>=͇$_wHgIV{tN+f !L9-'m|`4f${ l@qWV&nFQNӱ; /; $A.pƪ*ǃW1kdy'" :VSKJ#ų'scyWٝ(pgAb !DyƯWMȏ(X5pPXݝ.#U*gi~ę+7?a\c'*\@swU6a@Cvi|}ˏjKR.9jB7GYth h qv8aR{[ׇ!hFlypvs ǣogް9~V΁ iF7ox* Šyh?ݙ[-`S. YdԾ6\B谬.t;p8gm8/&t8 B37? x[_.zوչND7&qfG9,@;cȠ De]s]3|z7 6`- Y617Wcl5[Bep8:kS%v[BHaokF5 &8e"tgA;cy0乿`s?;saClե!Iɉ{;"pX;ոn[6m͈_6A&88Gigg^{РBًKw 4G)~X`\}n#,r.2rXqM f.؁s]6&~&?fTu){h^{V!:9(.#tCa7Qєe8q-'\gm tTM~&?јq13-chB- Y: 蔔ALsMq I)8^KГN}ԛ݄>(Bk9p7?``sRܐշ*?WGP2.=M`5wv!)Q12lAn)́9?X 1߻~V'8s ;`qW1i4q7akf7a8f/T2&Oݿ?>QV$WC_z.KaJR1M.e91w~44~/BbxP,1%>ک>Xu=N*ﱟ+?ڸjFGyт筵V-x/y3#뷝ru2 X i77,qz I&7;-sJNB1]Ny"A`sٚ{)_ y&hqicm>p򤡪kn%֞귞?jp񤣪Jw8,ەi |֤CuxMe#zcr͓]>uQߦۿȠ`{neqVcԢ4uvnL!67ScҺZS6V_urFMwn_lv-o0d9t:k=)`dHw ~jݷlB1,&.'۽RFeuo抩z]{t$, E~As|b$U4e|H(Gg>m1wwp$BѨcggBuy&m{g`OPeFQ =u'8B47\E 7ђk]8+ }gNǞ.vf]m\H{]i,_t[R!BTō_|-Zdd{w?W,X5l@'OrJuDzªM9>}ⵣ'4B!J],IMhmNi4x#;aۣ"D(VԦ=dߋ@%nlBtt@4EQ+UKqɍ,R*ËݽiayDLq<ҭ"_w|#;[cgWNf#F['azlbRQw/IZܻWc $Rk{2¶h/M]HU~#ӷv<(NTRYA!:xsXy bӭQTO;SdөeR^RV73&Ms,Yz=_W$LwT=ajap)2P MgREVZEvmN ҄$ rXo4jN l⟈)$I+$oaAx ʅ5#;AÝE/ƕ_W^+akP/Kh8hAw]bX!X?ܳyMD ~ 8ijnIᇖVHeQ'*_Wyd@^0|e9ǖDHSh_>!IRnb v=OH;;g w^ޖOKE",#1˲C{E}t/$2tښI$BI @ $IZj)-z3gY9zY͠0/! ?3JJ#Sw_)>ao !aekr(kNp87[|{EKΞ<{8ئ7EuȬ ɘ@g2=I&=,B [8t$L]^Ko |x;w<,88@3#,, ^KpppRS'4B|w@x;f,;1L @3w,u%Fﰷ"o~)= y72lPke .KUګ fxh'i^S"~`u"~6&zYҔ*sժ?M[sG|ާxG˷9.z}'Гťxj#x'<ɲh=F&%0rW]]R̼q(!\3`6xb^nR^ M1$<7CV'hI~hx`fs$[89g~ba@~ڸ=gw>ȿCB Aa˱n =S.z׋|vzqgVZ;G0n+J|*1pCE$|?)ѯh/睨k{ajeof`/z)R%~Gol1sSJqjJJN&$s&Yt5e*zQg!vQ7DdQX$"uJqI/{7*֧y f ~u^}PBJ%)OV߫%Cl4w9X_tnX hU%Y\cX/Z0+Ŷc͹u[sDг%1p41r0[]b{Mm.fnLU氄U)Jeo]%#?$*uu=̏l#cT5 ;ּOȊ4۰kVgh ӝF:(ϬOڞJʾz7m_&nO788JW/ξglbD^HmW- oZef3IҨ/la+x@\J6.WOпE*y h^&%3].ĻK=k7;0A#AMtzĻfoiV ._M0Ļ朙uYX& 5 WX*vl ZQ dn]Z8& 51^Y*2U9Yn]ӊvpLkqMOR&hV ,±…/+]I麗O+@xw @xh{`/6S?X^r' YG ;w @ϬKZ/~w;BCCCC7!-_{LݰbDFtZ_Ȗ/MM:M^X7޼vDUhnl n5~c]N M*JpU*w:NL uwrn2aࢅ<.1wb-F,㧍 p6Eiy*^D!^! zwncZ~$ht(GLӎ]z3!wqAQIf|:( )kکLR7F1B~ƌٸۙ;CQe+G93RYJ L-V2Vs&!CFۓ &Jc3Ưc=|֟>_lv-o^;Ba7qI 8]2* i?nnnuQ•.$Wf Wz jm&a'T%fiFoZwB6~RmiU[ʿ6/}ES#D1ş.\w퍪en*WVzMo%]#̻N8Pq~ VΜԙO 3+o>[3f`[DG'Xu҂D!vۀFj&S\ *8v^HU9OmU9 i{x=.pʼy icoQ]Œ`!< l1::\KHǁ!v}NWt3_Ps&]g;oӍO]iqA>$7+*,++B["B}>ˍeyDLq<ҭ"_wBZf͹4ooUjhLotse"d>lMBtQJrQwm:1B4MQ_s~js&!!2(^?rl]9y|;eNnAݫBJra ZtdsrRƞ߮&h/;[uu}㝲ʸ渙1ic*M}e>)HtHUgS 3ׇLRxlU<<`ذMT߻t{w*9fn`\O lAR# !gjgk3r)kp" o0REVZEvmN ҄oz4вC MbQuմL21HJ#(TF7~?t ]S p4⌱o.QEExZzF=yMqܱMo{?,O L3H&FAefQRkG"n*5gro͟:~B߇4/9n69afJa}24yq6e+k7^딌vr2]1r-9;u[7G%a "d9 Ps=%WTyryҰ25v9y Tmnᜋ;|4}.<!ػϓڋWk1J fkɡLxu+P3׵*l N m,s1\x~tJHAi>dw&>Y?<%<ɦtqĻw "oy?3߾M+,0Z C5m*GRE;Ѯ6 "YKi>i.i閻cߐ= Ox tb I_UUض(Ӵw,k)-Ue %tH-r=j æMaE"N?D͐;1 ?z4Bi?nnnuQ•.$Wh ϕ+(b~\B$umM$ JGT3qY•DZ.mXy ubI RN 1Pk0>}/>_gi=tIsSɅZkxC&:$?:g@K]i,_t[R1<ɦˣviA"m@T^!0u;qw@we؞ k1Ocylab R@qҶ {uny\XHtHtK^y+YTχ{1;Ĝ!TgazlbRQw/>~̙ߊ1Dȸ}gٝJG:Yi/;[u:1]!|~o5+5Ju{BID;߈ˋkjZV&R Y=O]WU5sۜ.Ϳ}RHiDh#Փ8J&맭Z:1]l ^1PkH*q\MR1͝\9eiova'MM2iB"XB*R52cg"T h"rˈ8(BXM$تޕPO׉Z։IAYN,#c֐4!*ݵ&$u;z2g;s6o`Ut5-8141)ƶ.N+pdBӕWO7(݈|ϱ#"4ҦJGZu Y?<%;+銈_GߌB0Bs>7eec3@4XzPo!mlN9 ~;[3_dd3w;LY|;7V1#C=HAwP @|_O `[ffDU~GbttD Z eGt)BH ,5TqEK/tK.kFGpζɄmp?AwɼAАaj \ :1B6(kbv>_{+B"=j/yB ס *q9bZz ߷%_SzܙOdvDj.= Huߓҥ4BG;3xXAV?|q룖V{:\bGd[ |ٕDz,1Nyf^1fF*}ɰU(N1LeWlB!an52{:ǫ'a턓AatoH[ՃR ,l\Vv-[֦EEV}#Ug֞ UiI^i-6z,TAHS X>|`BUב,fΛU>#f!DF(?r2q؈Ndw8M?o"hv~0c.HClҎW>wk!5c=vݘ!!GQӿ]XeuI$L]]9ETQ^v5lbD[dZE\Q~sg{_v× [iV Ӳbu]H}J\B)NJ[jwkQO^0rRX=(5{CzmARaʊ(Whhkle*,BEQ.{W0NRR%͌O[X4H 7E.-xx:xdQ*f)NXjZ-ˊ8w.J{n^HWϦU[RZ-ʶxoUPjkw|ܹx1{RbJ]UΙh\)e'jр=5Ґ Zԓ0N89)'F 5{C64‚2sV1>N.(M( D e!P;2Ii)SE VUHj6U"ktVYD c.R"F6KE%j Dr Ll_.Zcy"k,_޽G̓gG\''BgY9vhm[q۵;!(H""fHvJUp_zUZQgaHY :4V= &FNJӃpO7_yzPw w3;(ܰ#D45 O\uϭR sn!J.41BHbYU1ĸL<:({B52ބFa^~Io%Nad' ]pIգeuJyزmL%LLPx~ŞC0Lf"ǎ'=܈ koJ R=jָn-IR*\*+d[YtUKr ɝQQgΦ0WDk1Ӗ?{1Kb-V[1 +0y3ל̳A:o͟:~B߇=7">uU~ްb;|4}il1g*qn !ѷF۔E ;}صZao@lzΚۧ;) *Rs>D;)Ļ`C٪.PL 4 y_/tZ4|Y؃!DƉ9H6 &Tō__rKJZ5+oLbԒK[`H\u (+ 7u!DKRhb`""@|4 ?@y^𡞪f!TH">JpJ x{E+@Sai-(),w@SW񮖆VU"FΥ$ zL˷^8?/=B6:E*,мa;nx4oX9 oE`w߀zLݰbDK}yI_H! ^߀mnܼu v- UxaKwzô?_n1RY`ДZR4n/Yѹ}^IE ՘j~rcjTOwޛa9l^V8-J4ZXݧoO +(8} <=-WfȂޝۘVf =]-a[Ӑq}ꠤ_,Blkww0Ii;z w FEVكo( [nf _#z ?_S}[T: WJhm"Q!İo GxnK8kӘbu?md -JuS"J>vę/\H;*҆'Po ;ۘcnŦ&/SL= Ox tb /RK?p̻O#<ϗ:yZ F(a׷.tE9S"zOg¥"_'̻N8Pq~ VΜԙin*WVzMoBh40ci&<*[3f`[H]l`::6k煄Y<Ͽ6/}E쏫"͓\`'m{g`OPeFHc'8+lX~|"]v}{(2/-z|Jkqf/Z5|.Z\❁չusb"ҡ#$^4vH|6L($BO{1UK<|YZFJ)^nm,u\? b22nYvvFˈhZs i߾v24;r2,\v6ʜJh( TݫBJra C#o WUYVVjR9udU NGf*ݾT+q 8%*AlikIbXnN׸2'B)5D\HfoR'zj$" ӝoD U3I\~LUr}6 moRFTaf|7AP~ҩ6]dءmQ=nV 퍿4@+ۏl9TBt;v$BL*EW|MqzZҐ Oҥa Dq5eHM#4wr唥 j RORL"i^,^!EXm T-}¶gO4Ywʿ feD!c+KeÏ2L\ztP]iynxp-8ߡ25L]gDo,Pټ&IaGvT:n1Zlgf[ JyFI @ $IZZw#=ǎtNZGDwxN_.QHʥ*dU]̨(NϳmgS+?_;}~8$2tښI9yTU+n]~6ΰWoۇk^wŜĹيo͟:~B߇=\_x23()H˪G"n*5gcYGNmSnDT=&8̌Q)LXUyryҰ25v9yb\HHګSh0S#L&ӗM"3NLC[#AU`]m0:K[OӁoy8ؿN$! V.j{07X%0:ZJѲ>j=y[8X^/Z]K;Uo0:K1i-NT-|1tXoɊzK1_R]F;R5Z} aܡ.$Ywˈ&X`.lYb媵=igx~ý z&jS" ;xVśZ:KuD;ݤS]cl~ mℎ&  5Zoj,iwHbwZYI>tN黢?(jf:KMИ { :K4|;%ޓx:K ݻ: W  ϛَ7tdXg[wR)4<+W  ޽Sa&"i'xPG( {&apG!蘒fKV̲ŤZdXݧoO KZX{:vg vݧ]Lu<>BI$8+lX~|Q =t#dkڸI{-\Ļz w|έ3K.&BIuItTjNnVnF4EQꕍb-&BЮCs "BN"|S+꘢̉bz*T%+2Mϕ+7w -8=ݞO.JL*76oĻzw Q}5!IKߙ4*D$I2H!`$I'T8K|=LYlk߮ @>[?,b^SWoPe + %k"n*5gtT?4]wqNȺdD@39A5Uiwֆy_RMJ0TC;ߴ:3 K>^%@>,I:O^y s0 @k/5", M31=nX1%kwTY|;ɪk׮]jv7sε3viHev9~ٜ@&Vs m~)ޤĪf5LCmD@ڱfʵG3_|W:~X^C`]pĦ&QgjUFvӨj5N-MvjREGJ]q;Wo8& +DZ>.݂Q}[9J- )zyڱ+R{&QB(XDBE ;ry\b?5R[?N}/DYg,%߷P qqI;K l~\B$u\'5+vX_:Ak۟]y,ۺi#MhQڭcQ8iR׆ wFYeD鵟4"˲M,ڎY45_/fW}Aܹs` <;뀙cV !Ka2etwM3q9į=5H/$!i9dv#[\IS~#vr1#e agEV5bcsqq̬3ڙ,*k濮fYٛu7SKsG>#:@/#n3vFg<%/{^t}coؘrnse#gvyvijo$Khܵ<ˏ~>E WBy:TÈɞ&풳ǵ*ld%Ҿ~Dwm?x9մ.w0c-Q6K,ImOL($T~jK`'w1ɼ<6b*x~K/?q*J=nR"4g/q݈ȐkgOƌ~ HP?{c_wIxdU':er:M.'m}M:8#KBU@4hry !DZj)ɭD!Zu3,ЪcӃgwgiCߤI^쿒DU~iߟ%I(R,"ˎ(goN*"o_qș[Y&:Jun7e'1d+YDFF곋h7&ZV3x\B~* ayDLq<ҭ"_wazlbRQw/IOYlѲ>j=y[8X_k֖] b22nYvvFKu<'ٕgȷQV7+*,++f5^V=숴G7]>*d3UXBb.Q9mLczwsXu}Dmt4EQTfޣ&w|B!ι7/%QnYzN\aܝRJ"~ih4 CȲor(Ja b%%-2M.6lw.^9B S 3ׇLnRxl'Eb['hZbXڱߒSW꒨҉mNp>)w^GW.rڶ(k+wBP&huqџq3c4ǒUnRŞtxRZ!EX] B"MċY=>{G{6c B(`-!ڝc^8ſm֩BvM}6W'{]30uΞ=g#\;Ts={6Ih~!?˞ޑ#š2ƹ*"vn3oF2kkU͚nTUqܶ2e7 ՈdG~qC cnJWyES'vIauGѩSߙx@Ѽhzʺ1MwGԖm2r$"kKc 6'2fn:U^'ZW0G}B+{JE_g^J6e)$rleoD?KvƄ{Tи8=wk2VoRB#>1>pðe_e|ׄ3L/vF0 ^9|:kn\(+W"^ ۿcԖkd8wE17vp"R`-ݺwm$3H" fQtu> L羳FD8]u?KŒH5p]B≅e^Y)O+q>zChTUu$H I$]=BtEqIyf;f靹Rv8顰GWvx)^Iкi v;O cRZZ'ޣ>r{tHDR*Wu^;{>n+Ӕyq!?3cT ;A4`ݻS/TFھ-,\?@%V7-:{c,Iw<)vCKgNꔾ+9[IT^'-6ڦ,q'a Gb/Ȥ< p[XW\t?čH3FhUȞWuqv?ۣX:,Z5*ukG7u)0٣f8V A/Ϭ,MlKMqӾ "$ܥkO\ ,IsBN~zP8Vb8\6],f3}7~qH߹Ϙ@yxx.P@(IOO0>f +Ļ ]-}305\RigOUò1gWafk~?a/d˒VMӤvnoc t`I9sSF6=g}Fu /?og;.5f@Ve9t͆- xCp *hr?#zLDa)}߿I{G1<"DSjJ4ǣmxoCEF[=>g(oҳ àUio?|i~ ru)FX(Nmm x㬎_1$e~E!Nx '+TEeWK*OT kWs@w%.LT7B)q;SUjvyº*l[[Fi;ܵxs:iz9ROwyҟ+,c6tt9Hǁ!v}NWt3_P ʰ=֮y#FTBBo|!TFlܹsfqYw(u28$~ҭCFv1'j/Zh85BmllYu*LѾA.xtyTxU.-Hb 蠸"̻N8Pq~ VΜԙO 3+o>[3f`z#y#K4.Q"Dm;ws%jAywVWHwW_&[ku<"D61yKE*!mejSLs2LsrN.6YJ`;*XF)0H^N&1.g($=sך.^RMS4!?>1n4"qu&+DqG[K4@"F'.;soŌ\N:}gٝTW(ҭ|Y1łrMBaīwEUg i钖u1nXD5cޱS**hBRV"-HHgC: s]GT#8%LI{IR:<?G| bTV^ ܎SѺa&–?Ӻ|6 -Kf$ʣ kPJ>)m{FZ%+m)5&*OVouTzew-gdn>=~שCpNL Rt&t?td`{&ҫ vGm7Cu"&h"rˈ8(BX*J' }v yUbQ(T4TjOE ΦwK۱a݀:iGDpcX :ܺ^SLW** ᰕ {OT^tNkhoǤGZ/As @7{=roTg3 <>$ {pL$^ލ;9)Ze.nH+6A򜺏ɢZ k.\RXP+a\׀AО֏cR ỄIaFwe$ߵsBM={# q=Y:{V+c=D9) VVւF|YEk [z& R͎H\ZT[H/M$D5Xya ^y B<6WQ%ڔI7KgE.zR=# aUJpcܺ~SLOʼPR-[;&g;ЃCdSx(?%ޑ-Q}}?n7e_QFW ^oa^ >n6`PkziY-meĻFDt_:*Oh/S3n`l3%A5%TCD})!5EkއhoPw1M(n%;]j ! <3wM.PNiFoZwB6~Rmթ,|]Aw@~ GhE>ZçƯޙ5q $!$ KQPZ֭^J]ROuzZҫ^VֶVYDQe ;IH *9B"<}v:3y̏:ߙ/fO} $ BGjt"[Vg\Yg/ Vא~N$ u8hV˵`1QcoֳsWZPjww=PԤQ?79&40tI{ J(?5`VJ?%JЛ@ @3}&}#GZ?^p۽e'is}}||ҕpIJݫF![`9#wZҔFS__:ъ_Lm9B;D祋/O^;c[IkQtzwּamcM,ADaѫ|gL1_1OsSxKk8X F^|ߔҌKǎS9p#z5NHAcM\O)yNt#KQCPpm{8B0{2yjg_W;#IRF֑~['^rQ/.aX_xbʕӝ q!)IL亵?KdQaaaOj{8ŐߎMw>B7T#׭rd #ޔ}Fo{aO6LL oX6,jvGCB#umZPZhE $jv=#D$Dlrx6~#+oxB{k(-Bh4ؚPf_޿ &1=Yק&]~.BA*oiS_zA(rؖL%\K-Seo\H!w/SMWWIa6'~E tOjUijbF-K{9BM M ndyQ َLY";& ޓ=ubK.Yğ+S #+Uݜ*d6 榃䐈 eRܕz[;4jn"l5f2I=I9;5G+J ov#ey|3]nOݒwJJ#)hBR,JQU-BxY]]Uc""}a#tKwݓǜ#9)Gϩ싕7](<ړw-U]i,<9)\KiC(VWUִVw0Gh^l|P`yb'aēf+)ai'&h n'O&IMi(,PO0GAxݳA前:9T5Kl)D$Ai*NXw*S!D: &B`,`bDX eBY l+/q?ȖCX6Ibz2EsEBZ`тQ0LԪt岊z"D*yeBJleVf bhUWl}B 1C*"!wuڐvyAy8MʯhTʚz1*G|pasVsv^ӆOX#ɥt58kab;DWmΘb !d b觧25(]X-.bvue(iB֏m%˸t՜Z\Ϟ?-˖fji=-V}|.\ݻ'?QteVk.ojzoC*϶ ^4kzP?>b]yq6WWf#3"^"֥t`{ Fˇ;﹧ްbg $&rȟ%𰰰'R5Mu.sbʕӝ q10JvlaH4@DڐU"gw5n"B*B8[V?xW4yz@˘|bWj-჌#84yeNNGo ;(t1Bq=iDئ5%V/xGFQ;toօmWz|.2FbG*44tyT ='ͶYnۿ ы\yV wخ׆m?S?ov-Xκmd'׆zqB̀!>h{#>vQV#ͼ!`K.H3˄Wv.?h!~~&+]zJ+~҇JJ ~Wzt͙hyGKt]IRR6QO)N%.|/g>Wgu~ASSy RTj2@oIWz8Z.U(*reEe-T@!mAGR$rl.t}FڼC8J nNYFg'iIjfa}h?Kf7JK!Z]?}".M[9*ue}8%+u˥4.zxO_BXU]BMRZC7~|YVr7TI }DI+z_(u]5HnNV*jv(N1œ48,7;67<^SR!dX*uKYZY_;= ^了[P4Ms [y-jLvb;Mb_:.fxDꠁE3T-V=8uBEw,]&;W@rl9V^ }SIIBi^qJ!Ӗζ +/*hK|>\Ir8R?xqDEfa0JXoXzزΕqk_&,[&+*(XJn{=7胿~W869ږ@4.Ȕ>R_WVSSBiy9k~!b.6c1'faU?PLfQp9|GT-~ڣOuCIVR};(nyQq12qɆD}".z@W|P';#&6h+)ALa;b9Ӈn &̨"nW^ X]}!R,o^;ï6w*Itx$o>D\EImpaeFtBQVk߄'TF?T{ .Xw,xz3t}#QXIDATa7 $tgqz jvlz'}1yuF021x um?o<1jhf :K=\>v/snYzh;ak:b؀`= ^#+ LS:+Z' j [KuyuXF&H3!;$J '[Jf632Q%֖'gao:_s޼fbωqa1ԁ!J8uG8Fǩ8`8F&3vr ѱvJfȤ Y"7wN G̃K3eص7n& B!+Wu}gddjVzg1Z[|+ILm4󴱙Z kK&mnt_7kw'=f:2p|3yuωuaag*m#F3zEqq哋]`jOKLfdchaeŖ/'<鈭 aWf2ϲౄ$a:jĝ>$"/uiJ1^rύRfe~~*TF&J33gpo?8LG `6qL{o&SALtؽ KatAs8ՈaZ8؃ Ô`2<}Psn= AG&:4+^Gz]lyPL1V4~B/8rA:h% 2[~e> ? ګJ#u%(q 1W~\,A@o|dl7Xv3nNS|j D'ȞfsUkl?SdzWEƥ* |}Pfe߹G%$_1FP7u3880GkY a#?QJt}ʩ2S7vZVξG?vs$=F,EVU 58PY{0:LWJP5KY; Qƅ & Ǥw u0f\'A me GKl×Dݯ~J60&0rg᭩(@Y s덡\LøH퐛^˖0nD?քhM 6._7_cʕwwVXjO?yo]&>)-M@?]!m|>fw8Ky; O1 pLeyE`D#/1/󱐚 hu e*#mMupq WJ&?JGP{EIvҕ\|Z͓zr 6{8ʋPیgx0blR_1=))efrIldO/m©|7~\9v䟶#nʄ2j>,ܻ"}-mjH4.  xQf߷W]ҩr lLd I51=e8R)U("[5۞N ɍOו #隼qTGʌԴ,#<Ћj8%UҷұonJyسm(pPAp8#} +=rBJR#Ȭ<ʘ,5)\:$?jAWh'% 3ou]APE1G_ƴ"O*gʤvL銒3;|J]tx\O 3uur$7_c{./jEiuc!LlQvp8-sriy9SNZ,,..GKz4c[ ۷ޮat<V,÷`01  g #`089b0 N #tIy}pȑ %&&3rnf# G^Vc0̃`pr`01 'G `pr`01?i'9KI#%K LY9b_4N]I%YHVd_ Y@.W5&kT^y-G,9z=F};ݤm3_IxZŵ1you .<\pf"OQئ:y+OYs?1`6 摚9wO|5Ly#.fDJ6ވxaꈢѽKFOQ5wr8I8JU3Wd /Q}Cӆ*#P ?Z>ڠ;l'L;;Z|{O]{pY0O}ϧO FY mdD 7"\˽9>H! }nLjQ5YJq[iO {= ^(tMm4v|^dpcэ/ۄE apw7WD^K]55s;ɹl__8v&tE1vO|G*>zZ*ZJ"\vΫ? iK{b0crW(仮yVTpi $\':7eWE98)wbjxRTea=pQABt׎ ,argguzLCUTk) Hxl `a8.X͸= BJԮOqR_ۣ垣c:) _[ i6ԕSGj~QB(đC6ܛ˚ܫeIԸJ({b0pϱټޙ#! w7Yxh‡oz r8]\1Z]5cݶŽw}1?(dnJT0R.U(_ E" @t #qHژ_`՘)9RԑVS(zVxenjMp)ީ:5g|ȆҺyU{}Jw'_4 4qo&F@r X|㌢PQ}LBYN즽vj*3x I(ri¨yaMA7-Sš*&ٓ{q Epe$̅ǼG7ԛ̏`f>rzʟɛyzF{~nAqt,zuu񎊦C:P~͕L[ ن뵺ڦɂZUѵ;%ϼn*DܦNɍݵAX-4[ayY+a_:IF{TwBi`-`3+@}ȅ l{ #-سYY4›vh[GԛS me=mʑ/Ϳi(NP 6'ӥhgtM/fZ*Ze^ Stq^lťށA_nݓ}mf_fiu}ro]׶ݙ4s;骳e>.}[3?SO կdOZ5װ&[!Ts+!nߪbO9_pL#ټ7I7{C^ه 6a)e2Os%Bf~ҞLWJhA8>'O{z+Z‡G̣90  &h[LO)`0x`089b0 N #`089b0 6ydi qvA j y089b0 N #`089b0 N #z8֮^۴m@` (ܣWoۋ~>jK>_1FZOwtذ127u38mߣu1N0՛{ƮU+V?BSGNwp 򘷺G6foΏfF.~H#cһKvRrp3ۓ_4sDkɎ=Ñ[5_Kv4+IGwpۧcӇ;q5qtu2{<0/eǧS9׼\OT5 83׾-qw)cK @y^b)ՊӌGd{3;"B8~q!vF`>-6f_eZɷ8A2Aq?vݱ#]鳔>532:@؆NZpdTL`_ \ zi6Nt_s+q)qá 6o_Ù>Bل 9Rl\G= 82$Ͽ`f7,yLxcYOR-KE^=E?|ϗ|5%#Ht:ω+~*2vGzX^0[@[>?dndt̓CwgkT>b c;ѣCE#\ qo^~so LMmn 8[.< yjω<sU&;.Ş#b!yBjHkh]=' WJ&?JGP{߮6P+3=TfT+%#y}#\c'7 hUj\&1;x|M# ^-Sj+9[gSp7sr5eM>wgdV%Q`,-0ʜHcm_mדÏ\i$oDZP^d.S._-Ss;G,.NpDGircdk!JV]]P[VK;2鞣)Ӟp"&!R((ZPE"jN ɍ+>]W* _5TtޟcQ=yw}#LI;L$ZnZۊ A7Ч#|pCxSgw@U Scޮo`6! +JRl+ :]ccM&pp{@L"==*y:뜤hi0,atLbPhZǞ+Q& (EMQI2D2{#:.Or͏,ߔ/oSQg+!L@$\Ziw/5' ld}9':,ayZI׽;DO^@@g7 [;)XjKUj&3 } ޤUhB/r,LTjT@B"TJ-m)6hDZN))r  /TL'Ьc O2T7/ޖ@!ZЃcMb[,iB%t@'kJv‡u!#(NNoP.è@pnz9@|"4e4Z_>eF?mb|1%D{#~#d4Ь4!Hsқ#DB@:<괔a{H}'9ed[o6H5!={`ϭ Ge3|GEu“#vȡn6g@+XIY fȘ3G!LlQvpힸK^ {uvЫ$ꪊ~;~&.G]+-~Ǒ=.H&p`:poqFƎotk^>r%8*OO? E(5巎PxYݡj# /1 brlQi%z U)_aZRU.u؇â(+@{a$ 0'GJbٸ "JQh灄"$*U mWZYvA2TEF\aUڋ n0dәeL&GR+p2[߻^j  rV;B 9jm*_.)YЉŤc߾m[Wg+RƗ;Q iSɭJ.rPH#K54ҪH&*γVj-]RmE\lH?:[Ij˫}aeL9UZI@H%Vg`&EUt3WOD]ON)EM>02 c#%1`Na$!LRLLV cד,@-QIa0f$J+ x2M@W7Tvx6PrXJUs3WmdcEV5&Y̑Rk5W~+5}9BJQΤ1F e(*dڀ&YT.f}c<2 .nuKBus2 cqttw$`2 [,;% %01LWz+erlG,Y`0mYhudY X"lȂמ#_X:ڋEOdGo / |0+<7lgMj:(Qk vBs JaqR~bZ5u_~#$qg3tcхkՀmy7Ơn,{Жc%5"~SmbV%G7]oq.zxO ^)4z^ᣙRX*yCt-y|{D v~u\ Vw+ƒ6u*9"5AyN\S;5;]RB<xFz $4(u2}EI%a|޵춂q!gs[":9ɅLck݁][՞cNtHf?3*wIj.rZ/ 7{☊S5$"zcs|Ze{s4u: Dfi '*sl|~⸪ {D?t`{h>33תm^7|/gB!y#k?D1ג~^z'W0(߸_蝹r kn%j|Q=`kȴ67HKX[`9[LZ^hD((2:.'Y-+;sl&`+sqWXVZj))m-XNt@Jo8I IDAT$h n%VkL`Ru2) [Z,ȫk6.ΈR;tP ][e:Z_*J,-iFwIZhAcN\X{eNLU5 q`x`Ӓ\RdسEQ:w&c̓axDq6}h\VRz @sjL}rugrL׻/ɞ~!TUm՝&^Z[UX^޸:+o;k7a)ʦ5Sq-Q;WO"(}xiS/O}6r S4UO 3uur$7_ (H^5oon 0+;z*R!8iN\Ma?]+c~At4+IGwpIjg0} an5}Iu2{<0/e 'p'߮;Vt7Qpm.YJ%_>LfZ lC'-[82XNg Sum4~E3GIC?UC1x =Q뺟H0 }3as˓~S?~5?S&(9G![8d#ټvo/p}x0ܐ9}2k_Ȗ۸HN ip}@sv"iy/ ډz?Rɑp2[߻^j QW!l\~{@]("o)JK+j,yLxcYO" jD;퓣)7.QRT4p{􋀛{F}6OrFNA4T,/t- >&O:yckMeg<r2՟yS܊%OE BNpHe/)I"{Uohd4; GuW#d$m+ҡi:4|+j'kvʹ#U*tg#mۺ:[upo"ex A!%g\/`JHWgx0blR_i\9j+r19<“u+zJs%ףw=Hb .AA%IQ ttӚʬt}=9VO} E&V))efrIlGZun e*#mMup"d/m'ք?ۺu֭[?[>ƕd 0ZpGE,(s#3Ȣ.vUj\&1;ܸuR((ZPE" cdUx#'o)igշtN~2tr̰M Fɱ)Ӟp"&Vw&"C5L#-׼"ԅc/vZn\0Zۊ T!TJQS 8"xĒ#S:I+\%}OʤRhB/r|cۑb0!rrUS4M$av&^ٱ4>!} YzbO FwΛsvjœ"Ȩ0A Bd~҃v[[C وMb< ~6M{iyҏe$S8t|ZcyʼvEQnZ£⢺t'kJvYy ?dvծ``i1±qt#Cˮal ra;.WoQa089b0e\AlLb/>`Uȃ &*Ulc`!.J:f u0.B&`prl\T5Z+^4frX6s?O8KąyJdV^{eihRZBiiL p&r)u4Z^Lzb/j_дfVYa0'?.T5cD.\yp i(H9%hk)k0LYV7W$Bר)5}~nhi`@(Jaj\{c0]'9Ҭ5j>yu6lIkzDI 똵TSX_<Xky_4sz<} ^Y$VjOkq3`:x,d^J+6Ϟ0e˵k2rXvXV{ XYuM cX4Hac0G^u<7`e5mf NX e5<3Gv•lB-^b0MURe: m_!Fdf9{$a4=D( 54Bl Ysj/hr-u2{<0/e Qa(pyBtWbǤw u0f\'AIwI 7#SUm:JC7]oq{᳖/|o5"ʍr9BhXu g6.=y[mVRl\Gڟf"KǰLSeWQ:ʍAyN\S;u(䲭Ȕ)*`F=E=r#>μGhX^0[@Ϗ#He/)I"{Uo[Ww~e˷#1Z#&{<| LS߸_蝹r zz.ݼq!gٙ>h$ccu@?t`{h>33תFJ.I.l?gR%$~uvmU{;eH#:dG;b=ZWSEi;J-/3eF )9{zc]VBZսݧfp&tN E՞Ok*r*Cͼ) ^-Sj+9&kBd*y1EzmZpGE,(s# *KPGD=FPIqvV}=9VO} EFu e*#mMupXFAނz<1/DjJ!ÔB݋aR0|%KLMD"~ P<+{k2`(ŹC @8v*RvWJwZ"Zj))mϦ(Ke՚nzR((f^bV"[Qx7 %)PN毚h*qdʍ3/hGd+T+Li ɎMЎ8pͦ:zH&Er8@ѱ^P 7zhFvCZ+H'?LEH{:{9zTt`>`ʴ'#$\H@}F zI=9ԫBLhP9HiFK6F!N?E5lTUÒfY9' \j7G ŶbQ_g2z7M$ɸώC6|`1RSEڳy"(FGaru @' eɿhJ P@H$J{RO6sޜ(dhl$_$d3>ug`"z5Ty9vs_wZtcVx^њ=oqaϽ,>1Vo"7:?"xbֻ19EΑQa,ߔ/oSQgX ΈQKәi o~ٳWObtbdtCIz P7_Kb\h|%M,\GUş8'0^E)iRJ;|SrhaP ʯ.dC3eݎr\ܝ*KZ6bJݙ(uֹ3 &<Vx$*#M9s蠴k*:yԧ1~h4|̭zːG˕D2Vw8> - qpu﷒Yӗo#qLb0^љݿ8]I1Uw4mY9Pse # %f>9+?VV腖!LlQvp85|;1O~z̄8uOA1KOhzF1ITUn>-pI|PxשU%U B;)x]ux v#rtva_ ##zn)tɇ$7{p77Q]F?UނOU&\L;Fn|?1TۊIK˜H|grL׻GNƍ[̞GJ\=n"՝a6d=X:~ANNxZoW~]VX4u?QJuaa˯]c[\^ܼ?UCht`.eвkGw1myIDATb;0oݚqmصX0n+0N`t-LWzc*w1 aNE6m|G5;i=ZvPxMK]G89ƮU+VG"=-FVwnxc SF32Rqщj1ȌAMj\_ ϸxv%K*E rm ]q*Jy C˕:=G"\?s rwܽ? DnK_`j{ h TVD+l:IY #ͷ*rmH.nsU:8h]gǒ5QGڹ>YK8ՕsGfo UXlUZkO5S&k'ktZ +:IA @vvGurmvQt?:2p1JKU@$&zHT!d`d;OVDj"&t'uGϦk"QtٚJwKDQ7Mk5Jl+&P4qɱi aI`}dfz# (`V&L?#,&OilZR!TOo΋%đ&#vR ԖT4 )Ӛ7j+@reyK`b OD_;nTUU Þ|)3i)hVd+v ˙&͡5"?'۰~!̟R!"9䔕 EB@61y:H5b,!d22!=b;[@ )֖G6MgbĿ&1eQ=yw}#LI;LU0̣iJ4*q%!@(5=@MARKl<&1fz# (#R |LJ7"մXI-Ĝ§M/kb0n0s诽G= ƒz;ĨNj X RYxMbtUb| M_]bTbM c]^zb<Ճ801/1 =#7u3X|?mi= l,Zi۶=!{-sѦI^8p~/]#x꺭_}(Y֙[ kՊUo#~Mק:z̄3';{ێ;'xֆw:)mOako|[GT-LAc/[M{vPU^o89rg}#[n"Rǖ'-,U#IAWYttlownc^cXJ4?z\C$:nNܬGx*\Ϳ#܁T{6hsG{(EQjx&jcpV E!bq$[!NZV)9>^ j;q_{d~ YIo2M$q4?r)t7׼2̞ d2dYf04RXϮ؆M[V\`3GΝ[jᯮ;2{_CwgkT>bS;#HYƲ6@Zg֕ݣ_dv{ޙ<.ФɲE,:;CTuy;]QjoÐ/PR]PI!Gw\'|3 WmAOYbqhY9) nTç4 ط/oJTK _t곕ec]{jծ[ƦRn6&e=dXm:}|2<3 b~a(jzt֢*b 0=GZIL(@S._-Sَd<)M~̕\ޡ̅kO,5,Yցk7k( (lswp#"=rTC.+!ZyBjHkh]=ml.׎>snK :t$!l% `-6nnwM==Y VӪD,3I}=9VO} E&F f3i1Zۊ IU`4AI P4lE 5>Ρ^үfBT\ܬ rF%;8HPS6L{2Kg:s88鬐CDr= td'!ʘglڼqTG<!S5\ [;)XjKUjx)Hs {.DR@"dt$d?Td:fΐè$-Kmp(`1 m,HGD,. rY=xY>O@8G3]Di/%㙼_ij/Nx+!v]Y֯ѹ0lDJh0ZxuCMk H79z:߼CEb !DɽeO"d<7esB6?ȅwdJW ͎j9EGʟ!o(ˎs]_o2sPUY0S3x٢ HMw~:]v};'op.fMc!PNsK+ý9v1/h2;:j -"B"BOP\sTYpMًgߒ>Klu(OSПIv}1pܦ{vdQZMkf_5IMIerK!buQՌ1^|UΙ^sMm*8h |s0g+7t){NJMswn(^~r-rPϴN0M1ͺ&np˳$刏.͌[ki]KAﺶIueq9R?GFDUg2Z<$K|u4б'mwONNNJ:=;w¹.@+{:/۟R}EFyc8٢θ[29}މ wtX !m [,DacbTW#$QzݕU+O3:} FFŨ;^qK5?ʯ~ ųٍPY1wK"G e¨Lj|Q8{*sEkNGB*ڣ2-V@r/ָt϶tŬŷٓxNUCZ 8{? #RQRjƓVBgwp# GxZ 0n1{B7p# Gx3PIENDB`termimad-0.29.4/doc/table-in-80.png000064400000000000000000000562721046102023000147310ustar 00000000000000PNG  IHDR1ƀsBITOtEXtSoftwareShutterc IDATxw@X:K[;D1iR~yRyiT5$j,D"6T)R1].p>-{ܻB @ I1 I1 I%/qvaxgKK } +J47\:kik˧+X撝^]97<_tXA2̴sdxVl8z_ yp317?r ?{%`[Ǚ^knѨy#v`+zʀb'cgl8/\wK0ktqD06oFԯo/4e2FSWN>μǐ anDV:gt"ΊL"v4w-?ɛ> H@驓Ƙ01V =iӱ* !B qIO,5UwӗՂ{!D:;*|nM\`asB8,(V{z[|P:lŢ;MF7/7{CꌸvgxB$oy-g%ooܸ񝽹֛Eη~w8M|vڨiַU21T6PU[ݤI v(# @Ly)rY`'2Ugd#!W-T64d\/㸺 MSEQ]M-VY.4Hs]n8HUUvnM)$VPV"pv@Lgwb1 &bBcǎRSSpL (Ǝz{ * &b1 &` $`L &"WF\<9|J(v]y2مw29n.A o{3uqٟx}5e^2e m;v($sHL?YU;M5jFc/\q\# qI]8xFŎj~oӜ O!&)YUQeW2f8^1 &uQC7Iޡ>L]Ϡ#FJ{\Eg*d*E=^G&ֵYSz3ߨP|[>X=18}ᑏ*Cܭ7V֐zhWNM +c5in2A΁6 @}fه^߬]l%]ԙ3?|NnMgy>u{y&ṁ"86|k4ҧ%ӓ[n`7}ڼMםǹw_uޔ\-zC1w0e~ʼv7rVJ-U^': Γ xkGdm"u*ٷmy< iX"RYۋ2"Iꦏ7V;>~HG,Ǥ)$ E j4B&Ƅ9$-r@!DSt;;V:5X{1 4-z !@& "G_Ugl劢#wi+$ eeEƞ&$zA`?nxC^%{!qE$l1Ҹf }2DmPv'UZglj ӆ CujOUE:a Qk579ǸLɭOiQSirM ejNwe3Pp<'W0 ~bD4sPwF26-(r[0yNâB,#3Aڬbr$8Kv3'hpw,ϥ+k̇5홰ח8zq,3ٵDr&'uqHs]N+md߾b}TJr$V` =TIՒ UL.> &b1 &}n&7twv_eBз&lwWgw1 I1 I;b'7hnڴiӦOu!WӃΓm߳z0 !&/!c'udT}g↟`ǎ~֍q&t>`690dlB[LFhV<9 }+vv4MŽ4Qأ <XZswo=c,kOʮxBiƏ⨫L;PvHD'%.i,x !%b]Xʪ?R {+B8f >٣dgXc45tY_~B!_Z۾cv⻉sG37gPOHz}LƐ~vS{vt_z{s}f-kjd!H+ q6 v_b_Ѹ-5P<$mDz +E㜋|MKҳjAԴ BN#>ֽfN!1p%>>qϧ[_([6^ܞFxabC-(?z@!I]!hz2M?g0uzeٕ,om:F=nѤp}dC!% cИhgɕbC+&.iu 3c*/沣&{T%g2W 2Ʒ1eώ}%q(ZwV+F3%zDpű'FN:z:5^5T;&"&iүhDŇR kJ1e# v4qӐVc⚇Kw*,>Yr5X9|O)]c7gjo_s81ۣnq]g"15ԛ[idRFWiZ?fp XW*:DQɲƶc,Zbt:HM#h~/N MѶ7 K'XWd8 &n7}h~7&vd f/B'GF_hR:]๺pA<~?VOkhe%FUs&Bݤ*urT*#;I1Ž&s11\ERg#dضk/bFtV"T DBɉ,bO$Gaw7I|9M+[ u>l8-r8H,`q|B,=$X$8v`6I=6Ɏ9ykI~ !D$I$q_s3kώp#Hys;p^Ī b>i*jI{UIȼY~rU4B6$s0GOu(/Ǜ{'0#h̭ =JF_/ &^U\^_#-S{#[9%ޗG㽋нN|(i98ܛ≢DŽ ?m'fn* $ 9\uuhޣqO1Iu6N"dyq1cW[^r5ց@ 9B&I;aA+vehD}ۃP+.jN|dm= ژ6J=r@N#JrnNK^.d =Snɳ渷.4r(5_wh, NkM!h6ޣ^}5 ֪c}9&7$MjM2%& , e2>k6̰Z.ªnh'IcÒxs.'Bi@hqi)j2*1PH!$'; W:([۸=Y^~jzĎL8-Z0˄'M{*\-A4Bթ,.%,$/~nkWRUPԗ raNl/0TU\ :b׼9&X-д4sF0r_]9b3o}Fgwq`#[aĤKS\MyɃBӪIC`z`> &"χoiAiLd0@;R['!L! q.ꑽn4Y V'KXD[ԪUdf .X^ӪUK)o4Өu#fJ*-e4lOkUR֗2bf 6U}Ъ{TbiCGT?U(7)f7Y{fd<$1-jU0!~zU>wvgֹ4:m9Uguc:@Y흑<Ԫ`!f ժ_$$$sFTs9r[Cן\Vױ:cZ)sUn.bVm3Of;^UU&0yIWcV0ʆ XIq6Ua"*!^֪ujoHg^_^ gLF\)VMtMdW) Z OT\v1+<1O))sxyqAz5U=8 }@zFNT\89Ș۾sUVœ.[] b IDATEL=1L$@3R4BC@i1EUvw'u1k^P7BVZٿ|زgGj#H!EB\¦̞4\+#B|Scqԕ'^(SSF Ifշ3C|"Í?5R%1 [blG?xjz֋Qkp ƽ1!5w/2?|\m``ILԉ)cI’ -ϓOrbR/C՝du+ +uLJG ~3fvoRet c?m[yV,۴3S~޴OyxQUߤʼ&'-5Ek>q]lO*g]dGNL+ܫ"j)') \@8n1gYh/#S~$q\g]7MfgwgS&BDjje;^Egg-i !Oxw: 'EfepWNдTq9g_9F{pʯ#g j:r{ųGH~j-#hc."bgOv/=*re-cNs/vAӖ-)@G9"'wYd|t5vA6غ$qfLΣRBb{ɹF $y[)3Yx`$Սn^nRVIUho{+h|hW Xp|V -ejB:7$=-pV>lV{Ύ_p]J|Ĕ$=U^-5Ɗț}/`A\;ۨK dd' =H]`*H:k7ӪR+ǮAwu6n$.S 鷚tFUe^݅l緁 At35EF2Rmh˫f;; MT4UWMե;]Ҫ08Э2r2)o#Ez Sh(Bz@3HqMقIYv5yPML,G.,eN.Nj3#yV:<5vX0!K5c?.h{ٲ:9߱}래~` Vz7Brtv?O( >nA0``*+46UA55;{6E {iUs;NS,1+RXb>!0, ٲJ|{^h;vݼ~{C=`ܩ gjQQ$H%ݐAH]tz`|G٫r\ERKUNIj]5*BQm'j#Dn}Cv֛QkDvݽCM&fj^@FBRW{{Dkjm~oWP|v_iR=r5}8|BFZSSKFE5JsDKFT!薴[fŤQX{rG̈́`I4~hku3>vgA Nj8}#uժp+꒍miUPMu2+Q5WK]4շD$IBFJg֣N7c੍Ilmí#,xN˲qH#T}fKlaO]uޣ^}5 ֪(*<+l{KZnRs{~wZt!KPx~nҭ3׵h5M4ŮRu B۸5! S?xBH}bjeM(N-Sn]>ށ7?^Rw}YQ@C;d-hU֩#V8zFlIuZS.LrɳFB!Dcݻ:+}[cc>9ȸ9 _򒫙E<2I rU^ٷ+KE[2]yl,S~OD֪?Xf>>M=dv=l5^?I~B䄅;**ᤩUi/iU#7Ps(ٍ'ڜU`@Lt@Beg#yrbZ;i[!xϛ'<T}ga+_ȇwK8-HuN=OMucOtSc.V; =~*v͹^6* >_{LBq{lvv1lOʐ'i+ h8])l+,`n Cl&PA.cAv|.'7fT]7[%qKJ.vK@rtX\UUeiTҺ_~BKŠdvF{Uy;p,pls##7Ac%WR‘IWv u~WPߐ`JHq&%TYmӎ 4HVi=+nS7| /+h:mq@ l7Ӹӭ/ -I/?G%|KEZʋUEdgKheYzʹa{ݲ?\^)>eocʞKcQƵ -vӅ}c⚇~X>|r; b="#E=UVԚziUSΦ?dɔ2%'&S' ݵ}Jpӵ*=:%%},q0Ӫ Vj3JEf7On,`j:^uZJ.v/pvbRoHH_y;Ӫ ttŢ(dYefADli=F&HNRܛ@LuzGB>Y񵛷{7lMpusj06FFLY߻'@e,q.lN:`#:Y ։8Ӛ9}^%I6zPBi{lvvQlM\[Af[J5 cP֋8bj=N2ogWD֪`-A0zЪրBuV*}RݢOߛu $q؉nh>W^|qOY4֍q1Bl#Eݙa70#=8/*$JzfO[yZx1T"YH@nvϠ>=k[q>1gYh69 E$Bq4J !v@W7-i'$oy-g%ooܸ񝽹FHe ܳ{'bA*  X07g ]Kmz~._t HI vIJr1|'͟ZG#̧/=;2~gMDz/]oӹ;S;ISUL@BX}Z]pd̀%]-K)Ƙkd^p}be=XZm>)%{C[=_#}$?wd_hib)&FEE *O˒w ~Go;C ‰3-*+tla9}nk(MN+j't7tGl3&YHSoΚL_p\Ho}.@XZjЌ&h+`$ea$`” !"#;Izbz3ńv % }swD#-‰0BlqZIziޫ^I  =:͋-hUb A4D|uj_$VU0a}ń{ raNl/0TU\ v`@1^hM՛#?j;}}ݧ*AI=byP*;s90Wpww#܍T*#tЪ &]XA>7L+$wim#//BRS{)BX^=ڥKB>P凘4褦FIrtGE='=5';K\,Ju0uy$[ZZ:cQQ`?zQzĬ&h0K.;_ԅGG1Lcݴ+ fJ5z  AC<%rPA-!wKFEM 2NS@˘Gq3O vbh#e~qB>!s*{H(/٩ڭ4 У2o);#˦saRsgC>?o mܚ ^$BS[wݓ`?y|}^3 95fG]q2|}+F{rPH/?r4&Yr%BYr&kXDwO>@!D8aT56f-fT.z(zOǙۓ :aSJcĘeT0m*m7| /+h:mq@ jmr RV03ơb.;jGUrqܚ~X>|r&Uٕ,oIMX|.04rVף뵘BwGL0;e<"W2 <<("SIKg̑Uɍp ol1XӨE!=t-ʬK.^npst:J7_$TrsU:a VԦ˱kRya7ui%[vVD#D9J(7VԎgj}۞jbroݨ&vSU\_Srum]‚F)+A#*i4gH"؄0$\TrTaN+(21)L X= =ԡVs+k}nZ^ĂG<GMgFK!%zx=Sw4;ѯ MAH$ pt\t@EѴ{hX]ʾ%sHqMقIYv5yPtTCn&QUW,svw!-NS&(K{-KFXM8gwX]7a 8 |!tB74 _#;k2 dijtx" uFWiZ?fp XW*h#^VhT>GhTHq=r;u 欅c(K.Q/YVc_fw˭;zX!&t2_7U8uP(5e3N4{uUssu̻1i $ZTs9r[ן\V[W.SJmXzZG/S)UcιpVjlgU[Sƕ:Z5533;-:ѡwkB>^x' zwx=8F?Xz;_͏c|ojjɨh&B vnHWz&GE ;2i,߃:l:NCtt4$Eܒ5+&}}ߦLyH5TK[yaW8`B w:n}Cv֛Q[~( 4B7pOsX<黳 !iĤY&)8^bbMhW_שq7k_J؅N.:GCeg}=/ "ݜY;y숚y-l)#goM=J{#y6V"FH)42[PƞNZpXJ8܋ے6&T|&}we^3^;w;R$v ]&< 0/vUD8]l_C/Cn}rdSU*M\/lH<޺(G`% ,s7r0 [i S/ cM|p[ރw2ȤXb౦G LG,,Ҵxt̒/p! /IA$A4M."hUch&T<[5OnH)= ];W`qEJA u*m{vLj9+{JN{wT&c] .2h={<1mz䀜Fޣ^}5 ֪{550eN145yE `d]IuZS.LrɳFB!ȑOtH-ev_U'Ŏ#9Ⱥ11mϧׯWoe:s#7a*U/Ƙ_s6o\>zi𪹓w(m̩Z5_O>n7/_ф2匛t> @ҏ͂*۷g7鉀 =|wA}@O &= &6== X{uuwbu]ƅ[#yrk1R47mڴiӇ'tQL`@ٔۨļ.*c^p1ɖ$tL^y ?eu](O{~m$8QX~߯{o0'8边:jq\U+>~>.1^1v$Lb@S&h4ugQf^Ԉ{)Hs]ol{wëlNM{6x~zrAW2\6y=/&]"ϕڽ+BG#&~y2dkAP?O*p ZlO3qVW/0S5dKKt7—U培%nJGD0Heʿ\*+gz \K™bۤ+3(o][/P|ܱ)X0piHtvIpcO]EӢ. )`olq>7Ι@䤅^;P$%F9K7bRҞ6Sj3/ȼk^3/K!oJNo,*P.~>IDAT:MDP-oD77׈l{ooxd]̲YCKV<X-oAFw?ȺZ22ڛD!nPTzZ_;7bβڽlxsuH6Wz2jgl%Sݍs׫v#F-fR`슖B ;?^RfiyMWwOڌR-J7Gy`p*R瞼0t5BߕᅦInc/ H\whTU p%̄{LolӨ!zقɬܫx_"i@7@³WF3s]ə$Tu'$zh˵-w(/͂JhogwjK:+_Z:L@YbH2 ɴ&UI81Qlw-:f)8-f4$N;V(チf0S!^$S99H5ĤnNEve[g9\a{F* -m?h}#0nي+P(itLD,ZV_z9x.*K\e2M;-Nlw")47}r_3E,>K33[Uw_,׷[>/w^Fa31UnTx<63<7N\it_dBB8؛4Zc+q"Fk (CKQZQw;Q&qU8b&jnո:&"\^ "^>jod#0tzwV $ɿB`q?'mUmZ 8Y\`lKؑm_ڭ觴VcDc„͹gGwԼ9ÝjvU2o߭w˞X5A'MVht`om<9ȉu>VN[;s+QC/nG0ZK8X6]{Do%b#/YpZN5s]]3X.n¦z)| Jc_8-ϻ"v_)W&{ZV#0\neN%=4c]NFe O`A[# 4 Pe/LSvRZidh%C.,YK2+t];ܛq)XE&XkneE OO_S)% Uїr{vx@\Jz2A@P `:Įx>PSݹɬp. JQW~>e}_ [-(P]%qP?@lBstI>E|@?BOtOm d c>oӎSM fJR?%Ĥ.SM JR%@pvQchA)G!lT)I VāL%̝q:;G(r$u$WF#1[}]oObҀ<;K;r]mMԇIWYIRg<{ MUhEՂYӣSNTSmZ?U|Cu+mڙ&Ha!n"Rw|x7kf\&U59iWm/ZɎd 7bR\=_4P*g]V Apcx`ʴE_lqKCڶAؒ'/(/ݙ]2<5; =)GS㯴Ewv+9B-hUi4gH"=֤Aڑ|mr$zyIVÕlU8}BMsO^ ުQZ][U۬4 N܌"%Hk;eڤ/Y3Ĵل(E ِP"s[w='0mjUfYԂ&GbZN’B&H-~J#gc/T?zОĤV5v)e{gbkZmcC~j}baZ% 3NW} {gL8 *{w7 ]58 fJ=% iUXrFj8 +$$pwu%lЗwZ229tKpܣ:& 7Iɘ} 8SdJ=% Ѻ۔V ics peڠ/aؒ)5_whh]DVi7s>hU!@Ȥ>r\؋,ApvX5Uͽ !v+ l & hU{}:xKK Wo0Ið_}nse~iӦM>\?\?@LGR xU It>@9l3e㟼E?q(X#׌7\yj ؓSm݈VV9x$-0&"y{[T-n*Ϟ!G仳uT-` pv [~Ol{οn =|$j> }؂/G&mLs}]8BOX+JOM\tJGWyNWj m/{&U﫮g%' *ggz_zeQ M}brn7oox?^Z;̭ThGX nk &BL]-k;IS@:f=Ї-v͹^6*ђ,g0S;:`pF'>8{'\SuUy}‚6>c%N-1k_]B.tKVd&Y^e22\rV(OwN^IHE'svJш`h_{ZVv/epgŜ~qB>!s*3vdc5̈ڳl΂CMQ{ ;>3h.园 xde~EMZH=>3 _޵55iEFGuG qU}(`N 1@DOv%1WTb\;1cIe Y#:k{Y_USǗ*y|v4=03IݼXbo9W8g <:LɾH6HBL @PR"ZcH[DnV3Bqj}Yf۔^ʾ?C[* }|^ˡvG?SuwTM|2OX^[u V:s%#ڋE'wDf弒\D/FqYIH :Qe^g?xRjLp{9:5-FN΋yV$%'u}]{DN@ϗ]Ua2" LI0($m^.PuTƀ`SCYl0 )i3UEe N 6>EALzЩ3v::dJN,&yD!ֆJZ4Aα.ePn/4M%'iQ%XݱN 9Gv=МD5&t3; \S?+%^{w{H̏EN/OTgk;+02dJ\ TvTKʂr^4~rlhknW8NT*Ð89SHAϣ٥Rz!|SAiѧ[EṮ#'GW7ޛ?oa':D8ֺܼSjs? w$a:rVk=dai}#7~.b-s=}ؽ|𺳡Q[*e)},u0N SH0nSwĈIc65H ޶;,&żg7O}}"@s. cD0?^PkfmX*U!SWGowVF8 ĮWDYޓ3ݭ@M_Y^䤱ⓗ^sy>3c~,ػ/G38`~u# ݱSlav } SL#Ζ8D(Q6vU ZNS8@[-h9^2<@ᜐF<*FſYtҢL'ҽ8yC[vڹcǟU!%Il}lbN;&r#@G=kWٲ  iLb/S۳^มz}ʆmش"EkK?m\}.rIJ {w. #e?_SzbvA&[JV[چ߰4o-I+Cgnݺk>[lyIL$tNO/#\tV%֝SM?7_~f !~[7r{x'vO>oj,_5}z}[{\(WCVzOY؜DTޱ&_5'G/W?ڵ.߳lFx)d[X 'mm%J&J9= _=kD}5}C%2INX\MI]nԻ'ʮ}B1MHNI=]݃lc"phu-B0y: B&TOG2"$N!]LP'}qJy_ܶ}gfkbMg/xKg|{wnXI !D5{A\;1LC0&H09I.}b3hD4x:d{%L"o`tP^M`{OGlugtسH8&!]L=fƞ΄?!9{O2 9[Ruڛkh©' `4#3)1`"ZX kF]ఁ- ]N2TOX5rB=d yX{7YLd섯>"̲T*iag "#͝Ug_x! A[c*(pJD fa`5P^wт$<%: R, ~9{R=>dDtj:B=^Zґ`$*x=^K7k}NNGbߌ ^?ObHBg=>NR]Ƒ/]}O*Ų-fae1P?x:u+HʂQਣ0v U!r=?hjC ^OEorPhm:qC+RzбnoNZY g#w LO;#b˿v: w=mk>=;k,{:sQ%<\,Cl cNܳc B܇2r=ŧs;$iX|j盾nS3SAubJ@/p|<^wR̃=}~NsC@N '@N~38$@N 'I$Ir$9 r9 @N '@N ]-fIENDB`termimad-0.29.4/doc/table-in-84.png000064400000000000000000000467371046102023000147420ustar 00000000000000PNG  IHDRMG& sBITOtEXtSoftwareShutterc IDATxg`U׀nf7WBҋ4A@PR}UDP)"/iDE)*% JʦMdwg (;lzy~8;s̽R*  ]C `CANC^{<2Å 9Aiܶ _?]e+݈Eq?ں 3؍A:,-s7:0M6[I_5W"܇Rf~9-CIo-wƖ5ȣ!n"SY/\Pasw9ɾJ -naM[?[lGAr9}~c{7#uROp`J) H+hac?;JV<{ܺk~d@wGl<^0aѬݕBCyZԡo(gvJ*_[Ze/eΌNBmN_~wp3AЬu57ɽ3a7N۴5ϋfdA?>N}1Hݦ0g^! QJMүw64dK^| ,ã4H@qve]a =sݥĬ  {rHK*Ӗ0bo~<Z~Z1C 蚤_>^ʲWTwr )<ʕ[ȻZq9%c[/Uixp%6]`;LS*Grg]J{eeV zԩ^]'탕+KWXbվXM}B}ʾ_\'?!S-BX#qul݆r-8ZVE!rVrARStK;D N7 @Hk g k"Rɚ]NΏk^(cY2<{/J{q7 >d0Tt"?/8bS|b_U.+9+p܏YYs^`twt,+/ Hsط}W`quQ{S\4(=t`O2*GINHL13k 'CQ@;ϓ[{Cem}JZgP:8 )sH₴,S_ٷ}k^;~B-?䙰"zNޣ.nwazԄtj&cL'=ׯ/ m6a vJz01\VLnᘥ7~x`()M) X wO{N6oqu;·.bFSyط1 4#8&+,=W9ӎ;pwjeu;yڟj.}:<'9b ?}#ƽ<:7U,0);'wY|ܵDizy |% HDA0! H-A\! 9A<  A `CA_yרQ0 H"22os6LFłj H皂y[" ]s 9A<  A `CAsH燢E.JP TdQe)#Ya;%W(/>`Nܿz姎;g1o=r80:CPa/(N^_ DCU)#z/`oG$}LnPn#}lX^ȜKXNF??Fѡay7zϭ@!Y ##]=InD =2T^a#i~Mbk@Α婣SG&2 m3=goSƱ9Yg@s _n̽yQ9$'8Kz++ ^ԳvK&m+¯V 3oil+_Z7Z_eJٰ@R8>k?}pp{ۃWjsb73A'1JdynYڽH" k[&n-;\WHKAs-[amKE`@1m {yS~_lcA{=b iA$eO=C +~J?#(' c~hke(&nToŴhxd `afrOAi# A8и=FoLxqNxgDW~mObs|n\,a?pС?La&.3oMy k3.f\cƯW#ADخw]D"@՛gsgB&Nk^r`˥/hoDBczǗcϷz8:ujLbHmde1\mzUDbEsx h֘m`К+QJҙVD^*{f;)&y=a bÄTjB6NSV\UeYUp{òPwV= 0卿:b{ܨ٬O;M9dnlP#ZK Z}}D4^Ro[ZRwgȘxە.4;/d~7jMGڝ?Pg":x:<׮/QqT߱ߘ5RXoptӔnԓB*b gZ|v:>xϘF*kbM*=37| y׊d οHcˋ_{./cK3xǙ/sUs@/l Ӄf:}UM9QXT+8C_ck[Fl_ ~̍)^ǒ2e_H=%{z IQJ,wȺPe!wHgrGIN;~g&6~ً6L!3>ABOA{(RY_FĈ*M oDSQg`g HMAF HW ҕb:>CDAp= `CAs yAA0! 9Aއ@A:|ЪfޒB}{ HW yAA0! 9A<  A y `ӻv 6}mk(Q:Ǥ[^$ˆEܖnn'ȴuf ZF-yizܦ DʞU+Vi.4ت\.4_nnCr5j&nj"] HL( CW261yrݦ0g^!jA])4Erv` %~͹::O3cDP]*4=Y0O)UW'GRZFkdf]Dw)L A?㙦pUH @}qr%銯g{6"&Q ObϾgh9dMiꗸ 9^Β?v8m<#nf#6㤼M[(>6tG+5In*' {zĞv 7C}|*W6d{ﺬ y}B}ʶ}&r<:jY"40`ӛ_=[5JLq mx{y⽙k6xLX6*&XM]G::>Ip&+.<5qg/1ڬ Bz/Mv s^nbV[^ll3ѱEn}{3ע3\$`kjX`ʯ((g 8 ρYqy݉QxIF1 _3QN=UN4"ԥE:1V- ŒlͿxW*#nZ=G uQ嚬/15)q&rr~3Wpy zFy!N Qb ebmYi\x-<빎ǧ?eJmdИZ`uVFAs,SBzoj,PPv]S/X5є{_*j1Ī_CtK׷ڮLS jWNer X~g ȅ mv'޼@@f+RSpU5w(̨^ N/ ?\K){x;z25N/hPN3ZuC<5Icb11 C4GOd}y\_m ds;cTA(le)+?6*O8/ew+s':c bb*ۿWw wS L@ɕV>Uxy6dGJl4X&XXjJ 5,P -S[c-妻 WD=/TNg]+gyy+ZktjD|m P23 ypɴYGp߲CmS֪k+M$bcy~.|H?{ m(ߨaV$LIQkP4&?JUĤ@ {@I|{񳳡Xh2m 1E11짻t"Q^#y(!~,3MsҸؚ'CQ@;=/eEt^ vJOF݆p H̙3.Xf)2*GINHL13e>oEKrR֛q^PC=ƺ`VͶ:Tg:K<~_t^xF~W]k!l+?X*&U|W^Zx)5 V}.(0;!6n%k1!40A̵/ y'pL/kuzRb^xN)0jnL>!q߲-F -8% ҕiW˚V 3 MV z甎/CnbF]^ahBAsM.x AUWmT41ՙ9_ *FZzHtcI³eFmAs-rx AyK(*:>pXzx]Z"t0/獬hb޴A\q݊:Bu/+fxu\)΋i׵镏^X$5oKK<"t0/׍hb9<t*[{yA:t_߲yO< (v- zyA0ueL)W/;ؑJ4T*AZ  Z봝 c4XrG ,wQ,ͣO^=h&^Aė w,OyðޞS0lwxӭ<ޛ toVݐ N 4q27l ][F6+O&-#?jրmz5[MռȳU1'S'u.sM[l|i tI7GsuLL1e<ػ1_S ꃉv!=:5ϹtrslC?NRp=>$/bō1Ѽ= vfw+5^L2\'j8^dbj/_/d#u(_I{{M^#Q;cb!IC{~;~NRޓ=; FOw=4<@ IDATO}`T(F*brS%A.~o m~j݇5*WS6iﺬ*χWphȚ$J?~3R~rTeI>V8t$s] wȐD{OnCu%agC{caF}KXCXZ!*xQs:GW_">h(غS]+3ڜ\BnX} t7b3ʴ&0UzGg% ˯$:2Tς&'9aq8mfCrt@u pʿqP֕\=Pv݂X:-&^dž#!r\Z+q5!(@Yb9R ׷WJLU@p)>c*ނvIy]C?5!_65gs:M q,X[.N 1Z h95$*IC<-bō:*( w;O;{%spUT䟱nftuD;VWOz$J(8=L WVfTWiXmOB $PԨO&FbD&1jj lHr[ )TE+{Ӕ?Lsr@P9udA;$zi_DW}LǓ,j!C{I%\=JQzx[rͤ|<5sKM.Vܨ%K}k2U>8[ڵdZYH$(IϬamD"SUiJ԰Y2`Tܫ+a@z ϱ)(J^ٻqӧ[yQ? l*ܫO/ E S5bJk#N=([Pdbh0XlcF-y| h,?"ʨէ&9!1fH\UN@bYO rڮxjlnbIsHK E"R5 ڮ#z/O49ё5{;Z!\Q a+ X]fnZ}H_P {hP@l+, 0r"-A._ !&CɽGꬻWvה[Ŕ,\2v5:mIaAo-lCM33dҦ3}rO4)M]c [/MlhEiNQQ'[rI/$ay7iZJ.>!?~r{,u$ZG"0n8et b="[iHs3SJ U EF8=&ЁU I:fx'/py:v-n1<1A ,\+/Oӵ<]K'-ߒrt&q u9n˃nޟůZiR8wx0nxti؊m0]u|V(1 `t/ QJMү[eĒqwqZLy乌3; ƼOO}ߝWL70d3*?{axl<\RFu| pkȘ >8B9IQ"wy u9|$F5 q^#CP>e[׾Rg[J wrs%3Ğ> GpQ'OXa裏>X7nqCZndQïYqڄ9>! tvNx"EI@9smg ޝ"up^7U /?2Mgiv@_n-`T1t3 F؂vX:)AKn,ϑ$;t<ז~FY2/gk%!:_f~T? 0:uE?TyڟgOfZX˶'vL N$㗥lPr-հ 3?X'j\Nbh?xGιo٦^FhTVKtCrXq+,\F.}oݜ6V7EOp"~'ޝ-/.;@ن Ba"J?r_n|r5K:X'j\NbA:z-u9|d!:_{dtս~3 @Š_f6W ig!߽qAZNW,$pinTɵC{N2 @$EHg6%]˃ܧfo = |J^{y˃ 5Qb:C4zyA0ua:_buAܷDA05>~V5:(A?KsGb i q PRl, Hsm O{bdc^jg=o…X!9Nm;$:? o xf:5A̵)DIcj3? +_Oymi+,wSC2ZƅW5 ` A΁ϙiS6L'Rrs p^\qQH?U/ND,K V1 93m%FCԼФo>bw@PX7sV ȴ`/zF2|,?ߒ6{\*\~h,ֹW}O5HV|x_ASܿ+`m^>Mhf]XcM=9t$f״R V4XuMAa=0mۻͭ~zo 59]~X.ˡ#!\PsS >V]A^6=:6tI7;|O? A >`C2hA' oT* 湮X኱7bGk$ )ϔðޞSlǤ=] __T?EQE9]#A0E]g!Vݛ e7d;sä8w` ai6C4ur+P箑Mz+?MeR=9cLQu %|h=9 >)r"HjAMxz;}8#1ISn1UO xGJwo,MfپHyR6㤼Mf_hI64mfDm9Yj>u6FrV41& 3=w}zTF\-<6T%iiଙ\r*ώg4V+1?2p EIG/3}VFO]hS֩/OJ7u~>]d主9|=`-z 9V֔RLkV>9Bwy`=“FE<:/哕@O8e>=WUL}MR2D7 ٶsoޜM?5}z^S*amx ]UI4ILiK+ %ר Q#Uy>VK镲*-KQzyJϵj-h fۙ?iSehzyt"?/8bS|b_UE4I-',F/ 9tyIDcP~Mmȗ!5>R ô"pĴ.Y8}t/wM˼=/!YY[ׁfW:t.GKh/ϥUٺWVvz\2vf+6ɭ0YSMR98R+ ewJ-Ty Vt4^97?$@f+Rڪj+ V=A}r^-@:-̆_'gZJѳXoMR' rZ>Cg=hv .ʆ~=cYV#&Y-l%Li܉}dZ^~!ZZn+[֔Z::]44{]Zo#MZ8Vi*N<{ {ռz OЕj '}Ip44ly±} (s_;),v 0fHOJi(LgmG9f1k֤ǽ_X;~c?m;xu(^dor.O 5/^\agki?RA=̱4&1/>AoYRαo !$Z,kVӞn[%_D;al[ sLv!|?}oښg_s0ϨxW~󣏏dz',F,oPڭ'u#7V?Ȋ=o}Wki>+bƘ>Ktbg&ʡ&)ƚ._Sup7 M8ȫ{{F{^t#VO*IdbknxR0d1fߒHed$Lwd+*ex`4 ^8ᰜɉ5 N,S R'n {ݩ8+J^SdbX 3Xs\3ܮ=s:/]8ICCѶcg_ Cy_%|Hcڑ}]T,Njfn5.QQe [v볯4tG M-,^4#8&+,=0DMQtYrܝ]\ػA&Ԉc$YW#>Bz;t n9CR>NL{EMęسI'-}}QC7~-*me%FSN׿%>m?N+ˇOƏ_^+ n]#jo]6qCL{$SxwFG"0n8eW6AQecLMA/ҡaKmoJݐݼ?A_uI|, ?_7ơMzy K>{f +Ihk_|y:84@z$i"~JCuNޗg\0k3i*WQ߻? "N/yʪDs-ABqXW4+l;6mS$I 1e%[t4 KdC8,S{%kzm^Z e+JW)`kUY57Ҥ [qb 9¶FErswY$Mc$,x+ڲ/YlmBtB LE:H;Ygjs *Q`V*Џx70Em' |s xzyhqPΎqIjF>03 )Z ([FߩgM;Ob\ǯjS9 ewsHyyжL]nҙ Ęt#+62LyҩZN)e\UEL >lbdhq&i_P⩬,4C/h7C >9!̌ӒdqaaՉjY0fd]hE"dGjz\0ZT z9ܨV׿EݨVk9߿gI(ϑ0eVxOf21@o]^L=Dt}{4YT `tZGQ*RJ@БCs/?늕U$DWSB4fWvt}K,1(`p %zpxOqQ['ߺbey;Ĕ?>jL6 חx.z1WSXS'<+w*[z5񙒣LCP& H'go۾pV[E3ҾncL =AYZx)ag~X\paѝKpAs]Swo{_e#r_ 湆s03Pp3ׯލAE6mpێF7~E1iyL_6Sy5ܜwa(a/=7x{|ڽ# lG~' In{ ssg-Yx^G Y0izbG}ؚ+{VX&ᯫ٪\.4ug>e';H<{c)H'L=q/)ÐO#uT7ܒEf %Ubސ~%N1tOSrGZgU!eT*0/+C,zTR' g.~w/{9[yؗ^-WR1)i陸ӣ*-E4FXF-<6]]DP=9cLQu \4aѬݕBCyZԡo(ghPhDvBֈ `ȋCAwR`Vx{+/v)e6f*[C&7\ǛӚ3\?9i6Dfko8{°'mr|st0 L0I_pY5q_7ouNA5mij/my!;ꪠp??rDe]xlRLkV>2yh;ҺohG,[9oTږJ(Pk_ zrųDD팩#Y @;֗_^>oLNjIeG+O{LzU'[JQ=&r}#s]tw7cS=?Ou[>RJ/OY鹻Vs^FRC-R ٸ6"<|@˂256VrLBa5V]׹y:q6y[Q?VG4Šv'0$#0Tyb5fg?ݽߙ"+rm)o~_ǻ/k(+ДVWkJ+JſVhLl^ϱZMZFn[MN`1MO-0Z-Skvۈ2ݥ#BL!v!kڝ0dX`ߕ/kjG 5җZ9YT߄L[5r%LYa6o԰nyqIU9zyIQkP@bYO r3ͽNJQii;ݻ;6nwتny}|2o3M$bcy~.|HxV9ADGTdU\ LregW#嘟}$LyNgNE7dB;G)VֈnmI5 MyE/z)K) \qUrОӅ gN4?S,;]r=ZccۢW- J ac|{X&ЁU Iv8ӌEߣsϯNJeiCt b=􉢆&; Ė_rbW7ϕ2v]c YߙPʪuQD6" f1ŷ$8fѡηfp$œK3XcrR҃uIr"33u1c,q u?qˮ؝/m6O·iX ,)?fo{]HWkD9 [o8d?!/:y~RE_ ۧQGai9OA zΔêy]A6s 9A<  A `CA>  ҹPs }KA `CAs yAA0! 9A tE#2 MQr>۳*RiQP(/ hQzp"Ap}/TQ9z\6E==|ZmGl>LWw6;QY%RA =dD߆4•ZX ]Uk򊾱V猹 6Z۱/uQ!M h]b4ĸղ1J6QN'(xxE4~bUhlj{5_F+xsok*=8J\}x<|DzRQJ-\H9Lz仔,"a&*wb:ƒ&q߈Hw+XqŃԆrm[C/UMytNCjDS<Ș~J-Ե-!?Xb7ScLӱ81~+cxR~818"ߋj^lyT)uJ7/0#ޖ,zi9gaU&l牅[}+NC郰,g R Ē/-L~j?9-~' Oaltc ^UHۆ __.16z}rft%A@4\UĦ_U;+hwix'< ^{GhypS=9y.<2("hB²9C=seON$BX8ansFcIQ– j側ўe[JH矧u;'% ^0AH nNsr. e٫U3B>=ӸnH0㣍iH!Lڀ[6|&}jpqpO|]18>X}|1B(`'tyn'־1z^ݰ%XOQ~{l'VPHOU: 9`sƌS̽g]Ӭmɰ\Н;v#(!ɥFtO3Rz;C`VAݻG;6ȱˇ1egKiXaIxS|Yy!:Uk;dĝ97xuHJ3 Q$苽| T){5H0kS05?ó=3>!A]QȡDkRpذ'F+6#Kq ?۟e?妁9ߵ 構Kx=m{6T~Niu]ҍ7ʊ 4F:L)BXEhƦ,,ɿtX&v /X}K6#2pݬ=!)xz/L <0n _ӊaj7jZ[B=fKLWlc.r~9ǒs DȀdOvo㌙K&u6n=m䂶iL4W Hv 3sV9 &;/)ۅH K/J)a;yGI؁qW&ߚLJ<.Sҵ_q+h\8vV_Y)),ޫ:$20:5+bCa>F4TҲ)[Ѝt}mY럁EJfѶ{ )JQu;7h쮧0mz eomV8ƐVNi=q0Pp#{#,ٸ )vY3-JoN0S|\_|Wo|=oc:UdIENDB`termimad-0.29.4/doc/text.png000064400000000000000000000753341046102023000137750ustar 00000000000000PNG  IHDRRu7sBITOtEXtSoftwareShutterc IDATxg|UپMR!{c b^"T U1fCBI2vIS朝P*  $AACA=AACA=AACA=AACA=AACA=AACAPAeAAPAeAAPAeAAP_N&!?Bm5gv2*K:{qYN"{} +SSS0I-`&;;1EI?zԝ˰ץa+AㅡƫHΜWP= S\ҐפkIiCP$to1P' =5*^oM>(tÔЕ^؇է \t_P=%S5̐p7L-fs e9BG^[vA! ➆7,'9A~:J:pMn`Ljƍ-:WWohxyr k)!v'wR4`Z%09O OI((+Kr+f!>O(:?ZjȂ:_QF@Q\$_;/`z u 3nZj _c؟$y0F"iR&@hu<:'1]dsEm}ȳtӋ:/^oM6w{er f[AEiX.6J6dræVu=lLjWmξn0wql1ml%_'&E赫|DX ' 7'8_oK*/D(#@؞]m΄.L }BE.NAqܲC)~m7(m|HF?L4p|=VZ,'J˴{ pPd'Jv@Zjs7hj "&=֜{[jOM qjv~WsgnK6nLH0]챃-"d/# l QH;H 3p]s/=9Jc,颏P jfgg&lC(Y&9_%$ t[g8y-Lg7v*]~]5;@Y0+>7AERzm8@d3%\{9VS{ˢCyH@m݄OMeT-kk Glj G9kTy$KByQO˘Iݸk'1RH#v>US$zs7{qy1)5a zlmk)5( o;ռsup4k ;KΊorFfGzd\[6f ~=cPSaPօ,n$A5˷OsaLK bhv+- fv,Xa s&v=NCT@G]+f# EEK8hV#rvy d\5^No=kf+@Ҿ|8RX`erb(-QjM=uasti\bg| ҺJ pOZAZ0,N9=5q H0[2j.[gH#APA#-  !  !  !  !  !    ({   ({ mo?ݵ;PmZ rFpX&tQ'|n%1?d_$ {3UN(z/]_laD+U#_>\fye v=j2 -6Ny~S=a' Ä"qnsꄣ3-${ok?6իVoQ}͉|kͶN#'#Z; -({K yWr1 TwHO_T$;>)Jw&o^<>0N`ɯ?5M$}c׌@(&>ߝȵXkQsg𖱚?lV [4J?U,O,є{1& _EK9=非)rwnzhو^+W_5 OgG|,&)<&5/FۜbI>'/z;^NԾW$.3$S)T_ؽe_l%뷒* ~pQʗOr]V U}_\7O,f-gV;P8y"ϗwK[}yz kӀj}x\ ^Z%{!OO ֤kJ#aL$jQVIS׿QIMXly{[/9]Ja~emUO1dFݹ Zƕ"cp7I=$G1o eCФ'F?ZyoW$bS}x*ыz+\44ӿJ"ϱ5'ֽyk[+]w$O~3L|q6F67<ۚb_3ޟ-ZVlw$&CW[]{]!c}s3 0/q#)3Ϲsu לs8tkՉO{/-џE|}; E둕,ۘwC}i vHY6#OQ®Knݺuxgz H^}R<WaaMPEϒ|XTTMɩ3n?3ex$Xlfʑl cdߎ'])2p@W]K*g NgOfY*`.McWgYCYޝÜ7Gvyyo·wy1ۻ԰k|{Ռzh,G~#tT:\ڍ71W]$!8`5U7:!ؽ?xM.L~Ծ\b uw!)ʞ 'Ke?2Ԯ8FKҠjV5*"9gNz`` 9ƑTW0ð$2O5H1˰,2, $ =mH_@4UOIUoI.tϵHO"/΅vt2T]mcu [#)dTr6o(Nf|xe9`և Xhh0\Fm7x'-\g6+*lN"/l|ՐgwA!#7n_*1/ώ9-g0rTfKw\M}[XژWͬfd\C 7t]Gluƅn>doOk* R;+k"kH-DYN[y.D:s^2QI.[n~Uۛ5+G1QlΚVu99U-36~ՋKp/q/$.7MYY9&{We>IE?ïpAßwߣ<;m>\̮{M Fd.*'ET*1 4TМw3?gw@iMNAACAɉ jAAPAeAAPAeAAPAeAAPAeAAPAeAACA=AACAƇ1.PXXA@p  'X,npg:bT1&oQДWxg\q %^5g#Tqؒ;tkS7>0=KjztHrgmn,F([ZPD]ۘZIgh4 H =_ :~MEՐ')O;`O[:{ {!_ʤAmS;dd?F? Eiޣ& " vfw_Y[GyO1 ͔CabL+W;Y␎51s28GW`R*L$г>;I%*&w|uA֑=1d=; 3nb0 Lֱ6.Vbޚ>],_e h͖ X&T]'LmNXo3[TA$nڵ=ٮi)®],(\',u(ɸQQWun?NYiAӦPbRt垎)Y ~="{^[s#iwl$޼ӤE{Ij +s516] &8gJy>(CΥLQVlX2=AZAHUYC ~3]kcSn:__ݘ.iH޻)j c6|p~׹/fX; 락,b?c^$޻L/mYwE5NmO' +dZr9J2.VTI3Om9]nudڸ/ޏ':aTV]Xd;pLtެՍ[=6<(iK~O<}~4uxZ;pwT]j(nlE۝۲G za=`6 ͒=Rչ*jZt֐A 6?UJ6Tf@rUTޡ 9C3 P_Ą8`jr /zbϼ2N0Y[(9e ףjVfDDp8xLܨyS!}/ĕ%;|b%zĖs uAҝI䝧P!J%blntĚfR,ܒ81TWkL~i B8N"NS&"l`X`YeYX s_UL]>։)?H\sGEW$'iMnd"%P2ƒ\*&sjU#FSTa"vQcjonev'cig/Z8kYII䝧-g2HHP_HHęL;Ou4~'8$omqZVwtێ~oͱfB(]&N?/,'.8$(io l;.)'/8s .=,`w]VϤsޒvsIӷ|R^DpQ$W(4T:j|рҵt$IM彁ju`iKK=ܜI(']U%e ~kN(Ujv~زM!%u R'֠7U{gՕj# )7O6) $ehɰ,O9v$g܄%Ѽ;aoHĨK+ ,஡iNҩS_bR{ˌ%+:WoV&!kwJ1A^?xDrp)#sv<9+h&ďl=N3cgjB5["<ڴO>3ܨ/.hȕB#pނ3x =u;#}}7,8i| 71);qXB%=n8iv !xĴ LE\*}ei>VAEBA~PC >sȲe1vǺb1u{V8V_x&9)|Kv q3ET%s&ujo?]'=5!9׮!y#(Mk㳆m3`䑐63᭨9Gj" ش^S;kkqϱ/F?7M#pYG~ol2twmx{X߀iSup0\Oazmt^Ɨ9u07ɽ\+ižaE 2kz9WȬ)FsF٫θT@;Pʪ3 , Y)`WG C*?ơL^& nNprIdzktkfG^U\#BU7ۍ[6kP]^[ﰵuu ز>WO)]BWϻG7dO\ոyqvK; j 罜pwi~jESz IDATxn@+*}!(! I-J>TB`Ɋ?ϜqCvUC:U0IJjijcfGMCgɭu.7gacma'S4j5^r kV:8V4f_J/7Ԥť\ܝZr kȍW|mIt zXMPKҢYc՘\ ~z3V/7])¨@ҌiSU 5& s4g:nЩ F l.ҴT:ro߫sa' jJͷ^B$4-5ЅX@(m3;Pu8A#yXV{mc_48n>bh$Ik9B72j mClerQ߾ҊNڭ^tZ 4T&!@fBJE"s&zښPi,JQNWN=~$8طV |Ξ;PgéTS;b!]Oct8,[cfG(^W G9s?4lm`$2uGwnrB* H. ks[~gzj\9zgcX ˓=_fbORgտ,Oocl ,PnN[[fFp &gk4ᩨ5x˴v&Bg[;SӌA wn|%=kSk5]2VD:EL]tr@ P$ܩB"† 63oF6? H@@TBΝ@C=LMvUPU?TdLtQ)jNNMkfGZЧ oEpc;w ]4}ڲGdo/pwlΩ(7_{ܚI>=tSheǰnԬ"k#Yq(; =Ac;0!9xs? Bk(V3{UF=dgȸDs>]M>7gvH :!CAE9eD[1JU.gLS-VmԼx͌~#|5=ޅuPTej5"»$_q51;mdžTFOjuhfE߾`A\1+6ΐs%]kI=oxd)?O}hȣ=D=yhA!A 98849׳u H৞Re"73 @YČg:bT1&cMuJ'ztHrg6J([Ze㝼}k@"rƄJ}h4W{vjϟoU!}/?_0MfhR5d+l@ϥ,3P^Þ~>7Vas[ ww? ({vb(LR?'9(Ї)t"y` N]wSݶ!Rݺy!qx=q 5׎8Zcm!kd{pL c%ˇȸ0xk]E Xs8pbOzs hKK|CKAP&memBنYYjQsWz=6r\Z #4pdݽQ+xW뤳0TA>5!B]3LMڕXuPI$pԨIv91%O$'jjYM"xBW)Q贕ˇyR"Oouh xʤNWZ,VՓbdR +Q&d,0wPg-5 H]P!^N>NBt/MAevl+ƞ8Ao"+d`dn6&Ah##_TZ@)+q A\t&J(% Z q'HOJR}8֭Fت K;?y]yMTfGtpԠ9Й k gedzTDKApWmes}u ԝQ =zϜܯ^:zB&W0qFn|F>l{ϔV&^#۪eL^)B[W{mepۚ5U&zK!׆CV"sx-(4T'wxyG 9kEQc'&{͌蜿o{Dz@J7gvʶ( V䵑Bo)Q"|'EgU5͉Rk81B6R6yK;9D/[!ZL&fw0^{ 1U{_m{23jvk,S۷ ֨ApBEQk[ ȩ ({P0 ȣ%rppS:Uz6JJbExvḡ#Fcb %BmޒԪ:t>c C"1jNiʊœ)2*̭)θZ*tVk"T[B9wiocBj%}mFy՞'C,e&x=_~`tA7_ЬVR-E}kNйҗ'Pw'ϯ>LI\!;ʳvyk`klg UdF{T&^n?+y׃HZ*vw,I»{k.ntY)f,3yAU:wrp&Z(+f:x{yHNˑQnд梘T歑PuܔR&2Fo%7\4&!ȃ%{@8oO1:޹xvݷmeu_;.~RM\75Zg__Ya%Lx63:$ {7E~at߆:4#4" G9ܟgfZ'i$paf(N/ʖ8yVd NG\hװEt*HۺnS0x&mؗ_)ғ b5h &'gHOɉE[r/b3\n [YŚJSrDNiګo<7D|Llu![}&wUύ]nѳI*J.0jr ifNYpsw&IlqB1$_@rɉnx{ԄHn#=8HSna Qj}*B]3LMڕXuP0BNB\>̓yz;0MdMA[h&N*a/yCV/V}Kc:ˊ9FPQU-l"0Hj8eXaY8־}^r'z+ˊ'q|MbKufB wu/$0GT&6h5 2)~AzT.(/''g!pPWAb=h &L!5v*d`Rn8ݝjV"g;9Tձ*;CRgSR& Z4qV+N访(<."mWܭ5OͶ&-OL<9(jIYlצs^H(GQ$^#H}hR)+q A\t&J(% VsЄ!ȃ UBBӳwIZfߦ6Vi٤jPAR$C" 6ٵgSxtRk><8jRNy5qlEbbU𨱝LIv~ڄ#4-)@@:u u%TgU'%)>]rr`.̯i+L#SURi4 Bnǩ|g4ҕq|O';x4tj/le^p@ Ap- !Zʳ?9َ-N=cL5UꒅSzl\Xg5q\Ubb13I>5d9}צM|F}YuE\;^/=4e&/fX Trb\F#/lLjh wrb {_leK\߰zͭ <.};0ymw⠒?}Hwɉ$6$D7z ur2W{fwe> Ν n;jACA&C !  !  !  !  !  ! ({   ({ A:TJ#jl8䑖=b?RɚQbrDaڐ_&4rkšOu]:8fK~{u3s>ȥ_FHn1 ȽTawW-Nptb+Q?^zkQ'd=pW"z*MNjHeb1D?߫4mX޷\ϒaE?z owO ?R#tZ0Z)O[RuPsiȞ>'CUQK3:cSFE2ҋ8? nν~}=6ܫAż5A7d(y5j2pk6M aa76g5.+v;Ƙρ/n aoOS(pioƋW2T䢎>SS$ 79GʕDW~MSyK :ye17m9o&OI/o`@q\{x+).`O p;*ek#,@صv5=B~M'ge|2ef@}q~g1DL^0kpwu;.@|gEEN2ミj%Ŭ~g~W q6STb',߾HyO|{PUf,~99cJuY'/pmX28h9C=Ke[Vd,}$6~w",htt4^^j:rsdꇫ'l39͎ ܘnyVسe~9U4}MCnI&}J|&͵9{RPyk^#tK'8JKʊ+:I #Ei$ .z t{Es47iɉĺ7X{9i7_x5$٦B_[]5}/Ҟ7X~WZjUgs}{+ :ZK7?@ܱ[XeLL)KzzjikW>ں+NyK֭y?>aB5$Mo6  h>"|_L@»è^K^84S򪔟8[m)\Hm/׼Tu_2 I7}yiH)0h#|{L$M&_%/SZ" Ph/D9t < {UܕMiUyq;A9;S~붜5ߔW5~HayXHIYΫuϙqsH7pPkO!{ėK|acYx89CYZF)Թ3̬!,aNmvMv<{2SU'/WFμ'KCuU=^}R<WaaMPEzƒ|XTT.j Vmz]ݐҗYEɧWR@ʄR:Dg1|{/r.,޾J ĿۜzbU޵qk):\MH>3ͬ> &h_%Ncnd}: ) fcA[79yI'HDF!ȁ pp2 r9S*vȫc>rvުRJHɵdMSy|fܪ IDATl&?jϿ].|Na8FK=BFkjjhF% kqy]}(vީ:h Q)8sSj,\u.Ƭr\OkY EN$4TztpJ5|C>Ctstņ :ZY:yo+ͥy2m6^ ȔTqT{6ð$E)SHj 5]B.yМXK8@Li~< ͒=N]apo>xZ怸jr:}NWV^KuE@fXRpn{xL<9!t¯m& `8(L,KRuӨH$k8uzA;-iy=N2klN1JXQרv߀TōwMFԔ[ @`H})gng9AX"܆<6?6dXj `ˤ]D OG< ͑=\,=V6ax}E#h}WJa ҏitX㟖8@Ss=0MBB @':t--OY},gwg٫YZVtEE?ïpAp51OylgcicҮ6_?i뢧?-g3.%uitxɰ!5~-6ƭ B%f1GEۓw.٢.q>FW/O&WS4҅^K_< ]/XܕbmHߔw={1K'˯mXOZٽYj0'~p% |{^STQhUx$g]vfˌyTD{KV*A2@APAeAAPAeAAPAeAAPAeAAPAeAAPA=AACA=AACA=AACA=A_G1Y]D}c͗+DHoܬ{ ȃ,{֛Yځ3tIOѢ-#N^A䡗=(\g8^Q(E#(<_AaCh'3J`'t\o\$ip0y<8j|ArqFJ'Ըgѷ) OsAkY$?0Ǥ 7';qb=wtY`-rVEw'B~Cz@gry e3%HX%X,(CǩDDX`}ϙwL#ʩ[$E4SG7Ϛ8f )"U&X ZF;$xN :2%]xDO pegU5f[">#X gWƔH?k+tx̳P%gݟVt~^z@7|"I(-P'|5c 99F o%'0MUUϛ52}8Ax-$]ٰ6PbRq(I+ P{/3RQe,+ؗf_, J76ș @YfMg]hs;+kM_~4"&ӋɑVϙqT!aA;zToQ h` Ƈ%D3;=OWz'G0G1 . W[>4v6i\rv骳$8q*1Gڷj'|}jrȯW 0 p,$gX3ЁN;+sr"~)Zd) ȃ/{L)jMRSejӢ .*1}U]ϩ&yymmȄ7 >Z9ki%_DY͕skZ6y9X+$-_,eN/_A F`Z) B))` M{sr RD=,c\)#=9#;~}aa_QqE-r״\Q ߲RS+3LJsiqPTPQEf`}qa;WNs{Ι{&("O 84sVT% x7^2{PO'/6& iڻ{w},B&5gwsvyPtv-Pa˭zZ/V]!k;5RÓ. {$KOΘV5,B9WkYW f"򺅆M !} m7̏$`<4W}@ Ҁd0"8U__q\/ڐ  e=Y!P({=e@P({=P({@u`OϠv>M Lj+W{/{K&rseD/OZ?(gJgnC4dBޞā*=wDV j/CRwzsZLE7^ntuѸeL4YOTR :q8yh8)'ZՏݱ@orucarb_&t,VP_4^4j#D!}ke9VaYc_asf+)8ƎKU'VXUeul# ޗPE NzzyX3+Td`OGYnGʺLWocUtJ8jA7qJ !$n ذU9k?n=vܿeU6dٷjGQ>rAd\[e^]R+dEȈMU[ iӧ S[Joټ~3^yWQwnθ* ! byigZ{Qϫ]b^7{Dr|@!N){rl!n;ou܊S9a}%Ǿr aNQQvNgh7O!u7^\u#v B+6{)V}Mk^{p~9n攁;sRCc޲qk!_}q {- f,_NCīW2b":Otgסo{;t !;b l~a3j\3G,1R 4⾽&IߡokB,Ue߿_8|Bh"4uaoK~B _ǗhAةol\"mDPQ7[쏠,Q]RJGK̝v'v%3UvbòS?X(y}q߆/H!\= 7U "g,RGI,ҫG \̌&YzV2~鿄47/_ޫ,(VP WP/zu#m#z!h]Vue7o}|$i6ʨViMjF" ;M"}<͙W_Pi,:WgHw!wKex;C_QrݦXfN2wL#KS(KI™. ц"fx̏G 빙vM>|ws!ٌovRަ嶚ۢR^uv&Ʋ?&?ʃW,3vW O[ZȭKvRD*$FQ*#6tT9B6Tk U[ܫ#G[6VGEr8I9((rP) I7O!D%NWuV'$_RP$}e |4 dr?br5-hM@My)!#u?],_6~gC<$f]GHZjilbve򥟓/ #_zGtJIB7Pbnz~,5BŞW.SoCҾo *!\-o 'e-C=0UyxGac۳FwIٞzo|_|@q"f#/hF PU%6P@zRʠ0Џ((AgJE$B"bQmmJ55e6O5i"8ڲrQg<4- j(!VhgBi՗ؓSun璥/L'A^6tg޾|}B 2 A_cy^ E \U)v䒒݆tXQsF_Ȼw]@r6ӧwֺRfW*ѽ_TZ^ahTrASv٬ֻmDA}V(W(""LNK}eexQ%ɗGh/"+ ԧ//t䜡?vDI3.@˻cV%Ο:a*ݢ)WdH$Ws̝^*a`^ϼ$"Nnz)O[vkJQ{YO;_W%% tt *gRjIw;zw$Rn،BdOؽ?/~9n˥g^X0m#5FCIaaս]csEr`U=munj)KQ^e~kP~Pʞj}ǹ}8ި+UXE\:aϔ A1i"WLL3\IrB2O_auJ9"&SS 9͑ow ^WʨudA95qk>wdpӲ4cL&{E]5٘z_#7D U/}pNNBɯvcotrx&Օ_ʒ./nʞO\>m#R}qgTO\jp:Lxѳܼ?=Wc|rWM0f6O'e= >W]- Q`NS֪ 6}bRƌyj ˦V}k)@٫v6߯Gx4흯If<'D/m}/vw͛0rϖs9e˛kַay\ (3fw8\߀܋zJBD!G*W]sywfXa4rBdDZRp|ORzC8!FդI3O$q,E!J ʩu\R CL7< rw~/߁VO|beHP#e0} dոYG8!;'! ]"L$Ynfr `\. %x2{OgֱE<7Œ٦&FL̝j *uķn y*잁Y6LO,3Ůvvl zc< 2mNY瞛boC8!6#u~bdv:%LT > Z!A'}&K^BnNȫp\v:]bO\;E>aigh9%u{u\lmO77hW{,.N߱#O25&nf ajDWxWVݿ o}ÐT{!Arn&S :BYbw>I݄#R!2n뻙 3NDŽxbnںwιE 7J[wyA=:~,\>3q\qcyfp]B8!P;fysƐDlsT S"<{@֙8< SyuƊa(S!"R@#:z&H2M8b_MbO)! b)aFx&K܎{Oygl)(%?Nj㇎@[F>Cr=eyB 5=="`1f qBQ5Cy֙mݘ|]Y&!3Boa$8 GZж8=vTR΃̐pr_(NK7u< GCjŒӁxq/ qE.0(oM1t;3Űwo ˲Bʳ{8_ITy$ !&c 9GPC753rҷ?4;p~Ӄ_h|ܫ4mc cdqQIdNQ3 QE%1oس~Z9QK8~{3}ƛRo3D0#tDaƓa܋|b{ֱEn.0(otfô܉|b)fbySFXޠ y2Cn;9RLO 'TIDATykm+6"l;,&'~Pc]BLOcϽ"K9@<}Z`S-˻y'1X ̽ d7_q&eꡝJZwܕ!,H#$0x44EO޹<{j !cg/o_T!3o٤bx/!J}#}'kz̑V1iZi[%n׾ZB#Aa(DZub"ѽƍm$_O(G^<;"C{>m;\nUsJ#B}c xZ!W(LڳPeFxXiDFL~9LϯCǿ0mLۜ>^~BAEI/F> $>\g}yƽŵ3y6^p`~cTguʁ^sxI *(pEM.ȅlt!=} |nIw!wKex;C9mu]I:mpP7TB&#?@!/ʎfr8(fs}<%:1_!/՛NϵtJ7Ʋ*Mz{Jt4B kK4֚[44"9;]IihW{l8!Khמ_UsO, *Mzynfjh$Ӕj:%e~:n@zɍewzƐD_s5#T 5BU54"9;w3qBzhe=CO,&MeR$X'"+o$/,PVS$~{wOO:n@|گ3|kIk͈5W"uNYfG[ӈX~C(.P @-[N[+JMQm!t D髪$d| f6.hhx,hjS+ˍu%5Cųm }GZH.D?dhdRT[$ӈX(.D4B =;''fR]u!F;v)[L/Jx)|iM $ PHȐٳ;Iؓ;2q˩ߋO9FWaBȡJMLBjSVV&F>g uا4B 4[;9 ρ#ڴ+AxiDТuh^ x=e@P({=e=e<e}>c|5ЄpZNa AG `[[tf<< @ .{9(d'yyTT Bq}˗swnk0 tE{t_?Eq]jh~UǚNo "mj][!t[tŻt+}ɾ!34;)i<^I4"O%JG]RXJx2%-~]GL1wGrBo(C~|XRk֑D}Cgh9W{ȏ=@Kګ"eyDyPtv-Pa˭e'p2 F{W}@ HIqjh =e@P({=e=e@V@Еďߍ}&n i->vHS[>S^'+=xc$>G*>r'65o4٫=2hu:sximN= %9Fz%\@AQ&8wdg[ hx\f(HT7ݓ6&'6Qn7i*{(~9g*>m{\F.XiC}vBӸ%w-'n""͜27t~N*3o8'|ڧw(I(Hq; mmGEȈMeG!'ywa1߹uOk؅pѯcx{uFgr?I5N ĮSJm N^? F9RB>=K|asx^ɠ^#۪:/6;J<i/-]Oٌr@!B1`ῆ[ظa뾣\H#R}~8Rl4=:wAiտoYszΤ`6`UIf o…P ?tVDԪ*Z*+7Ũy:ݽ~2w֋7^ 4MNZ|XB,q'cd"$H\ƏrkLZS*$pd4!DM&J"{jI=5p9SEKNxxz*:)]eK'lbyI"9=Q,uZsMu:=) da[>&?ʃW,3 A~BB(E!m'yg] nuaٮ@c=D}!GJ[wŞǺuS4MCR4LUyxGac۳FwIٞjitpoD O *HO#CJf@ zyF댯Woe83W$\G`2Q_?gy3. !1DN1nrtCӠ3<}%$Ru\Dp%tuz*jv!:h@ ԯuQFШn<Y4BtuZa ĕCI2VLr<>㝛gu{eH!R/7No~bfygX l.γz#9Fsiu˱_}_%vख़~+[WIvrp7f{O7'7*-l?QquSeL$HJ 1{Bl wBt1cuDW]蒻YޯcJ]J`Gj*ʵ3ynɱs{"\͞{pͶˮlbo63<P!SL/sӊ}MxL֢N>َ?iwX +Pj!3ڵ_mΐ{_}5p_}z[6lI8rdڮQ#NO?ܖl+F<9J>o=kS'NB?e5V+Vl}Ui &6N}ic8M³dj:( EšQsĞ& iky4 aG {?G~N㸰H'}&K^H)unؘ#-=S< LΛYO%zݳM1LB2NԵ'd)ɈO(PJ!J 'v:x݃4nfͰmݎ .j8.1uip7cµT7Ʌ4 )/1ŬV0[g~VQ5D]zr4y vEБs&zV<=BđQg㯖tSx0t`pe6AMjutY3lhAط(6'}T S"S֌ f2&#W"A95qk>wds8ͶYu)tRݨ[٤m'(+cs_6!d=^D)$= Q5=Q[k\Y7D%E(Tui#߷O| ـ/ B5nR476xzMNO5r8:~TѢc'L4%6pCdf5t]q4b)Ҷbw5usgEk{&xeUTlޱ"{aF*w1k׫`΂kFXxe]+4G>|4f[aFr8#ևxw0B .usL 3jr'V]•=WBXpELsL 3je:F-{샇j*{!,fp^^@bCX(~c:ֹ.k3_e*Ms5̈ᮤ l ]Y&!3BD0 u8W{=64maFlw%EqB+{ݹ}3NCg6p#|'2< !o @#d-W{=e@P({@}+jYL$IENDB`termimad-0.29.4/doc/text_template_01.png000064400000000000000000000063131046102023000161570ustar 00000000000000PNG  IHDR(UsBITOtEXtSoftwareShutterc gIDATxZyXgvY`RDnp1xJ4jlQseIXiSD&iQsx-%(7"˱?D.1!ogfoC MUPTT$l?$~D\mqC0o;wHRp ٧WPdq Y7RiY7K(n-$\5Sv!]#8ǥ•F /9׈c87C+U''Z[nQ[ H\=91] M+,doOF)RVxg!#p) 2pI$ 6K`8l^EQM i!RUOȏ4 z.Jq!qIMH_N%*̓oB3ށ RѤxȣx1v>)_ !^@BndZD7M{0:ɮIylgBWO\!&@{DGj (l,' `jRրBFs#h-|^*z@ԁ2 $:u r$} ͘jZ6c WʋDJ־/qmk갴0\0sP?1vѱf$ K3`Ԍ(xoKx_]8)*)g .iM+H5 hDztaK_Wr]dJ0u72'sd+D0?BD[mF!e8a5[9ZtfLG}f7 (>Zȫ@ j:ͅFw)X%*wҧ~>|9a`BC1/1ζցEc2 k" ۈ@0O<=gi5;Jr>aXa_ ˪z:-5g:+M&SܿYW.śd:" >'ݧFwBqeMWDu< 9͡a.b"#y|OD:=wKD͢MQns,[=l/;9&;;bDx L}f:ۭ:=佲 =Wn4zuW,3(پU2 ǫD`(gU./p_fo=ja_Es#|sەa rG7388gO+uƎ}5Ćmzq3q2rJ0iK-K~=Cq}^aJcłr\c:V-HXfbPuN+k Ź͍]R@طss+O{a#*4ևŽc[;LfD{9dS-O48GwE%8TL{اZZ]:+0703TQRm n0jA0՞)닌9p7FMHy x~e7NwEi¿Ņ7UWJSF2ҘX Z;?8@ʇ نRC8|h) ^dΣ<Vۚ MmNJRBUf$.MfEE8jܢ/RҸ#1%ػNl}I,- YRdGi^Tw Xd:q˦淺2۬UDt'`_?w{IO#۔c ;d-Pb|λ%#Vz|ZV\ޗZa1@> FIhT2f9W |P%?’I2MF+1^s3VV|)]- 2d3schPGK9jFo`Ď :yjfqV]EgO3;Q(/t̙9Zi>=L@`ѠO/.,RddeOOzg-\0gJ+m6zrOf Mʞ>V_hm44is{/uO,?z:yv4&g.~$u\IHlxiѦ;j:801sa#ˌp\صN$4-C=y+lk:E꜅ NKMEEW졣&ﯺ5IϚs\r$85$,]=i3=:3|/'kpdXʃ9ÛBoLFdYKJ4e9m١+䪮|>KOPT60IENDB`termimad-0.29.4/doc/text_template_02.png000064400000000000000000000170001046102023000161530ustar 00000000000000PNG  IHDR%\_4sBITOtEXtSoftwareShutterc IDATxw|TEl/B/ =>(TA*  =!}7l/вM~# 3{f~3gfA|>0#.`07 `a07  y_)&&N>}g_!&&q{$&'1~`0 `07  `=B|v ,s@U އGID﨎}{3EJy{'ܟ|ށj 966ܳ??So ^5cGRGfPfChs5(6kVqƔOá>T򢜕gm4?K`U+Ll޾ɇ6  ӱE x -2*7){|niz 翸! }849x擗^^ ̠vl\1]5YBNul(SͶVE,"r(nvrd#ʧOnS\B>{d\IN438ux-ҧȅ/3iG1՘2AwI|eSvװ> 5)ZH,fwЫȡ񶂚9s*){egsrߚU2&(db۳oG?AQl-f9qMm<ZϪ\ihyq4`^7,v3t:fe~q4iNFBƔ#Ğ#k( eƪ&ϧtD#{UȖCYRco03/VO"D[I(kXEn5Sv2]Va.`QZS Q:yς ^qaIUMQm ZHh|q8`h΍'I@lO"$bݑBN ]VKlW>-ԼUhQSH.g``Ԛ8kV=н<47 UVIpJsE`Fp`q6ۮ4,r{{l3p/>R>7ۦ m-- qb&=}eć]=]tjI`{[s9u(;pv>%ψ!~[buɊu iƂ6!مP?fxzZZU~gи|JUu+~ӹ3BA+ַPc{אCN64}@saѝVjz`mU$ ;XSC8Fcq8.!ˊ*5;.~>#k.f { d&^^l5̽' c+<~r/"1_?)VJ8d;O לUk~;]G95)aG敋WM gS{vɅc+;a>ψ&o['?$R&]$Ӥv4/lTɯcZ~Dz7ﶧ_8N ?.D_sn-4HXT|e{.Ie֛};;A֜O9SZv=Hc(ϸR'f*=u[NGW^QY)(8~VAxRRT  22K>#"P69O##%{"{mZu$ppͬN3@łxo_ڑ79H9f|;6dxVYRf(k>Q|K WTTC%꽏YJ#y[NTqxJ ]@ | (h<Y:ՖTk! Iّwf #_[xAw}Hd2(\x. !=Wb3l抋Q'wW "|vI?+\Nק 1%,y >y84͎mvȠ7PDp]8Frƃvo(6QKvΧ[oqzY6R2ENaTE$ҥFOix5 6*9G3-r[cj.&l~c"3ɧ*n{41tءh4mߟCBd2 s*rǔܟb`$6C,J"*d}c, ),WO4@WBRO$DĹWg9mj!'[YPr("}OTl ,_l0KH@7őx :K5!FzE.D@㺰D#lQ`L? $5)}8Ԭ5X WwT#kR,IxP>x@nl\q㺺"xv>[,ֵOeeWvsg!FG ԪĿqҡ]i V]z \ݿ(.V+i9fԫ h'ʖ7DL)M>T:93z/6L2 ^sf!h̃(f6bA[*h{sf<3}g1%O7S}f`t,0-~㣁g<78*TB JtSPQWs}۩A{5WK46װaRd-ښFa`CAFҁXUXËܤgDgVUɷI.n, M͕%'$L=[SνTVQuR砤>U'.ޘ^PUq:LqmI sSitsݛ3k*ǖ[[zᡝ#23^|0!]Β͵Vpztn~LgOB_PCmO>ߧ( /-RcJZἯ=x?]*(v? []bzfՠ=64T{NM05 {ߘ#׻ӂ'-=󾍧w.. կVzGIG7D3|уmPTכ6ԴɯoNz Q/DJPwǾ"^P̀.w /Յ3C!AXxl6zpYMfUUL>b1"3P~ÇYS4? w^>GA1q%w.?eہ},?6AR”W ELlhPvdBFdбUnR|UyQBMԛ+7Sj< rCPɋrVC "Fp#E꽱mɳvY!@^<˔'@>DL!cY(`lMGT=&Y"O^cA "_93D&&%ty.W Mt(j瘄><*dŎ_$93΃i 2)u,m~(7#!ώVpj<ESlxI80ɍ0fݹ. 2)kفGL P|5ε)zA2v?*gLD~ʋU'iE]ߞ@Ve7`&ϸ^bTǺy&zLVu~"X@oЇ;X jU쪤 }kbW%0M}!fU&<;)Ϣe+^Ŷ%َRUB(~ǖFhol|JuqǺu/tWIQ݉G^oͰwICC*mi+Ig/tFǠ_]禎z_=vҵk~]dɒϓrZF_-@Xşo"6Ywt?Kʄ^%7-pzs3Όo{#ǭtRZ҅%Jv|@Gp!M ۚ?'Mj.;|pS͋W |i@5OYԤ4r~!%u FpߩhdEZmH̐N7oB"뛔G_B55%:tsi]k̖*W,Xu\5 n.wP4$(xwYٶ5kIn;btƊ |/Psm>NqѬU3u:A567p Te+kZƎ*MRd6)s2t=^9Zn mCjCzq:c&ddmVd%r E$If Y}15GehYLL אNԔ|Ԑ}rF7uySܨ^*͚ÍȤFY}U'9*%e[jyE,,,1ә&F=ID P5OHΫ?}Dbl,8 7|h'uuNy UM7gi2-ysQg𑽪JP$u[o>masYf^C.FckT\x.*͵~oj]ɋ$I4.K"rz094i"PXg?__Ȥ-KeGnl` PnRR@)RfT(nT^挔OY4q|ʨ"ey]3 A˪NO>ǩ7N!@ B!@ E̽?$mvv-=@$,ΔF^dM2J3wÛ=Z4K4Zw)Ʃ{? ivH7 73tbA9Jň[ݲ0!Ue xZ/[I-G6Wvʒ3h^_WQsr}¢$_y|lK[&@QE;PЍOs8?w5כVٚ\l/@`bIԮ- ޓ"oے^VMlm)Di]Je-;i=C؍yU}9JCn~ fsb__BE႓"gc?FIqwK_T5ODe2tbfl]]E |؝vF=f&z1^.;8-ҪH e"]ݡl''z+Q|9=I97ے;;UGٱ23ֹ5|r3I1ӂ#>Ⱦ(&%"b7hu'LCqC8"~%y!0#j飕k/2Y1vk%}\o{e<@eeu3py_gg%0=- [g5t-&q'Pd ҄1LW LBg^uϻŦ6#NY?ZR!0oڅJʏKmaO-\a0G;~Sy_*j 6ozXM.' B(Oρ=y{=^1o\MDF.Y7!eciTQ;ˎ6:M|2nu[`qa먃[kA-IP[IVAX\J[}x}I:YR[ /ӒWsP~1E3\P^x)1Bd'iƄh2;yN‘ U0*7rRe;MܕXG wS6qľJ$G$L)Vٞ|{Mv8:cG_.#|7 VvƦcHΫngx|$9":<.B|9e|Bm[vCm;Nn:u}`yy؋o=߭N ;VOb@1qО"cŅ#3  w 3ͫa PgX}2bKo8 eoݐ^59N?m(9ٛFBf ?n\GO+-yD|j)!6w\q،%a76*qW|gMw5%/.J^s?;\t"eX´#k3~cgO޸(KwJ9mFܐ-ټ}}\j?0<&ĺl[Q2'~t7l'=VZ֣6b[=,D&. 蠍:ޫu?ֈJkܸ;̗%S~]u إ{ iřV--\ DP9q"E46V_}>,GɔJ2T*Io|/8%FlWޢܤOt<{TAENMp=d{=KљsU&i,Q gY?Ag*PH>'Ӌm8R*Ľ7AL `S=*a82?|9(9#& z~fbw%EDEk*gb6Hˇ?Š5MVǺ(\7qO`4l[[3{rLpbK{p,GO@Ad4 ҿWH\mO켘]|'ϜUmpX"Wt:h̴) 44kYFIPTF!lEVi猕7,3zzr R_S%$] n+b>>2ɥHc՚72cr07qq`puM#(LƴD?$̟)O糎#1FVh  J+:ͫ7Lj6R)Ȅc9QD~&xeQLPpLfiv5h.b-JYr ?+'N]nqv[1>ҦRt:=xEҺ7k\$rW5w/M?dTg96ţ\8_7y iyg6g:߳e\M{W}&NQ}{F#b3e 2#"%(k'/k*u>)ˀѭ2:4՞ۗE;/$A}]/ c>#2/v,3KщCN(?]wZV~&N>!)ӆyQVhXz&,Pĝ)sSi[vs4q'dg T@s&AQGmR{js/UۻbC$i3s o-ǰS} ':Y` Ww^WywHo=_\O2,񥡦9n1]) Ehp8;`miI^3kCCh&߻3P;;"+ GƯ&,7tQڎzj-J͌4zNkBSƷwEUAâzCrw];l|\lDJSTLr['MC[\xdSV#3).v@|+/z3#7]pO Uͩji+qMY#Wf\5qdepLwZVnh⿅1+YSvaM/rJOh+tSY% 遄 H!2a1%H>!f97H)!i=gihoN<{hW뛣ZA* f -+$G  8V !B,[w#!Bo--!$AF.fV GpQ"$E @+ЩJ!!@ o37ɝ'&.IENDB`termimad-0.29.4/doc/too_much.png000064400000000000000000000147571046102023000146300ustar 00000000000000PNG  IHDR5,sBITOtEXtSoftwareShutterc IDATxy|TU{^{URI*{HB "=@ AVGiimϴӋ3؋ڣ؎ t&[@@!@H*[eԾU#a~ssw=!J`($ `a0D `%`0D +`%`0_8 V|`0NJ>Jt O8W;vztʱeW`m߉Ϡo *g%2"~ QaLm-Q"B9멗0S>4x+Snb_v ^~Ñrk=ٯ[еNܴ5e^F`L_0tMbS/o!#nIR9+J9){#Պ?u8'(&M& ՚vp޸&lBE_T"CA*˱mgvAN=5su2@\KyH{$WOhK.zާl3_*fov 6ՅCW Ostv5/H\s-bʣ$#ԭZIy{e+9CMQ2>hT:,V͘)]uqI|"{&h~1*mpTsK/tǧ\NNo\;%%Y*LHRA`j69+-{ dxlvx6kT.,MEzB/3 8jussJD'k;%%~zů#+J;;a1SDލHԊ":1"1 iYP>-ſ&k}M<;[01Re<2K^%*v -\&V|R3{}}ίckgϡ!M&o~J5Y Tv{DYEuov7OII Aot$$'ӱ1/ p-䞨Lu?η o[Kv#/l_~%RKRd,(DKHkRS\y-(Dmǂ|f|# ƭ< -_q I ~{ĐD[^fd)g]N* u! :CnW HuFnK; `;WvWݍx\.\& 4k0yY C:WV&p=p0u~ @ʦ0H%1KCrys9k^I4>S%g޴̓Hבݪߛ#e5ՇZ/3$@ @\BeYe IR(qgg|v$K ¨jX:21ÿg4 "xzanN$?ew8TgdyEкCWT7 )E37z asB)J[ju|B 3x<3;^C*K XJXFV_=c>i_=,}OW0\!`0 T|/z/V[pBNiHSXI :B ` D xC)#J:6O/̵EbDdo`hih41&/D#ǍKCǠd٘5GkXusEs8PooIjCo_HJgxA SVߝ0#t6Z"u-NyvI(lC%fM  J;)OEdM xڂqR@laQ41eX`"H?)OI@ltޜRbcSjW@ seDLCUIMܼjC@&'ܜODipJ[X`ow^DnS-JӠE<}C00ݾ(#\a_DO=>c|R[-#7l.ZQloǶ-+Ox h'JMȋۑk")wA{[o&+O΃Q e܅$\"1멽_J,}Tz -8>vj7it;N]pJ{tfϜi`ޜ{ࡀUwxwj.顀mpG6ю7o7xvadR?_wq$!)Z԰@m Vi6sgI3~ޭ\X07;vg(.Ho! sGwgw<?B?vG~?ȐAƇ0̝*@7GYʋW\ U2 u+{ɪ [zq=dIٓy|Of햄rV߁p݂y7x W.Lt+sx&]Om1>VBNѳ: ?;9ڼi}]:RjDnq|nH'/޾sd?ڙk:_*;<}ᑁ-ϕNDGK@kJWYT"d\;6mS.^7xa,q *{d,ֶYB727S͍j:kfbư]xa;Tݦ1B:c>$}Tv+o6BJ|rt4WoR,Kt ّCqTI]*1d-LG7Y^r?ߜs|$ /_e 頋ZW+ s%nC#| ysRR̬KJBh/\2)6AL[ZwZQZ%Qbd+-1'pKlP߰Р؅C6$!%"H׾͝pՆU_Liy-xs[(i?oWxhu/q'і&U?';oyr{z[uL~cMJÇ3IUjr+fDݞ(eCSv<qm[_oWΚBk?]' {@X'0=**svEV^Xf|4i<=sF&G?wԀ.irܓn۝єUu~US'Ҟs;O׼Sy,EAvKre"[(?#g~k(_+# r%-نo4Jf]S\۪q<-j)KHv~TyOvd;ӣA{)xwp=s$`G12+_T- uO&DO^sCejekV $I}M}]ͬ|$R.R9L{|廐%D TӦ n{퐙\yMXß93vGPR0P"Z %h?;(URLo=i'Fۜ_sBW{'2Μ,kMJK&EkB-MIz gG 2E7IYisz'SfYndU(U =K+|3G 6tYXL,i]V:;u~ u9ya-/#6-W `Ȱu_1 ei oLe}~{MpxB>1| ъyDbǠ*7|~T]!JV% pd6F7@0VP rlAacm riCtUV઴^ dwENZw,:uƏ؟qx?/Sp'!{|jCrJž>,Q8@9f!3^ yi#)w's˕c{;{{qe3!dMòM;pTFcULz_uТ6Cc%X.㢐q-t}{ w5|/Ⱥ۴U,G>CƆ,C#z@9OGF{Xis/YYykD,N`ͩ3U%ω--窛R}֊PTȭsQZʵ2qG}b{M 3WJ)f/M69L_RƩ/Rz#PG2; !g\1rh:|YwvuO5:Bd;1o;,/|>l8)1 8걹G"| A0E¦W_8eAڙIuvݞo빳;嶝;O_aC#;Jt{f\Do]{`%1bՖ%2oxWj߂LXa0o'J`0X0 V" J`a0 V" `a0D `%`0phbŒIENDB`termimad-0.29.4/examples/ask/main.rs000064400000000000000000000034711046102023000154150ustar 00000000000000//! run this example with //! cargo run --example ask //! use { termimad::crossterm::{ style::Color::*, }, termimad::*, }; fn run_app(skin: &MadSkin) -> Result<(), Error> { // using the struct api let mut q = Question::new("Do you want some beer ?"); q.add_answer('y', "**Y**es, I *do* want some"); q.add_answer('n', "**N**o, I drink only Wasser"); q.set_default('y'); let a = q.ask(skin)?; println!("The answer was {:?}", a); // using the macro ask!(skin, "Is everything **OK**?", { ('g', "Everything is **g**ood, thanks") => { println!("Cool!"); } ('b', "Nooo... I'm **b**ad...") => { println!("Oh I'm so sorry..."); } }); // returning a value, using a default answer, imbricated macro calls, // and not just chars as keys let beverage = ask!(skin, "What do I serve you ?", { ('b', "**B**eer") => { ask!(skin, "Really ? We have wine and orange juice too", (2) { ("oj", "**o**range **j**uice") => { "orange juice" } ('w' , "ok for some wine") => { "wine" } ('b' , "I said **beer**") => { "beer" } ( 2 , "Make it **2** beer glasses!") => { "beer x 2" } }) } ('w', "**W**ine") => { println!("An excellent choice!"); "wine" } }); dbg!(beverage); Ok(()) } fn make_skin() -> MadSkin { let mut skin = MadSkin::default(); skin.table.align = Alignment::Center; skin.set_headers_fg(AnsiValue(178)); skin.bold.set_fg(Yellow); skin.italic.set_fg(Magenta); skin.scrollbar.thumb.set_fg(AnsiValue(178)); skin.code_block.align = Alignment::Center; skin } fn main() -> Result<(), Error> { let skin = make_skin(); run_app(&skin) } termimad-0.29.4/examples/content-align/main.rs000064400000000000000000000016021046102023000173730ustar 00000000000000use termimad::*; static MD: &str = r#" ---- # Centered Title A medium long text. It's bigger than the other parts but thinner than your terminal. *I mean I hope it's thinner than your terminal* A right aligned thing ---- Note how all parts are aligned with the content width: * title's centering is consistent with the text * horizontal separators aren't wider than the text * right aligned thing isn't stuck to the terminal's right side This content align trick is useful for wide terminals (especially when you know the content is thin) ---- "#; fn main() { let mut skin = MadSkin::default(); skin.code_block.align = Alignment::Right; let (width, _) = termimad::terminal_size(); let terminal_width = width as usize; let mut text = FmtText::from(&skin, MD, Some(terminal_width)); text.set_rendering_width(text.content_width()); println!("{}", text); } termimad-0.29.4/examples/fit-width/main.rs000064400000000000000000000073101046102023000165320ustar 00000000000000//! run this example with //! cargo run --example fit_width //! use { minimad::Composite, std::io::{stderr, Write}, termimad::crossterm::{ cursor::{self, Hide, Show}, event::{self, Event}, ExecutableCommand, terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, style::Color::*, }, termimad::*, }; static TEXTS: &[&str] = &[ "This demo shows fitting without wrapping, with wide chars support: *一曰道,二曰天*.", "Resize the terminal to see how markdown fitting works.", "Hit *any key* to quit.", "some Code: `fc.fill_width(width, align, skin);`", "一曰道,二曰天,三曰地,四曰將,五曰法。", "`cp src/displayable_line.rs ../other_thing/src/displayable_stuff/line.rs`", "", ]; fn make_line(md: &'static str, skin: &MadSkin, width: usize, align: Alignment) -> FmtComposite<'static> { let composite = Composite::from_inline(md); let mut fc = FmtComposite::from(composite, skin); fc.fill_width(width, align, skin); fc } fn make_all_lines(skin: &MadSkin, width: usize) -> Vec> { let mut lines = Vec::new(); lines.push(make_line("**Left align:**", skin, width, Alignment::Left)); for md in TEXTS { lines.push(make_line(md, skin, width, Alignment::Left)); } lines.push(make_line("**Auto (with internal ellisions):**", skin, width, Alignment::Left)); for md in TEXTS { lines.push(make_line(md, skin, width, Alignment::Unspecified)); } lines.push(make_line("**Right align:**", skin, width, Alignment::Left)); for md in TEXTS { lines.push(make_line(md, skin, width, Alignment::Right)); } lines.push(make_line("**Center align:**", skin, width, Alignment::Left)); for md in TEXTS { lines.push(make_line(md, skin, width, Alignment::Center)); } lines } fn run_app(skin: &MadSkin) -> Result<(), Error> { let mut w = stderr(); // we could also have used stdout let w = &mut w; w.execute(EnterAlternateScreen)?; terminal::enable_raw_mode()?; w.execute(Hide)?; // hiding the cursor let (mut width, mut height) = terminal_size(); loop { let mut lines = make_all_lines(skin, width as usize); let mut lines = lines.drain(..); for y in 0..height { w.execute(cursor::MoveTo(0, y))?; if let Some(line) = lines.next() { write!(w, "{}", FmtInline{ skin, composite: line, })? } } match event::read() { Ok(Event::Key(_)) => { break; } Ok(Event::Resize(new_width, new_height)) => { width = new_width; height = new_height; cli_log::debug!("Resized to width: {}, height: {}", width, height); // To fix a bug in Kitty, which rewrites the terminal on resize and // thus writes characters below the "height" limit, we clear // the whole terminal. w.execute(Clear(ClearType::All))?; } _ => {} } } terminal::disable_raw_mode()?; w.execute(Show)?; // we must restore the cursor w.execute(LeaveAlternateScreen)?; Ok(()) } fn make_skin() -> MadSkin { let mut skin = MadSkin::default(); skin.table.align = Alignment::Center; skin.set_headers_fg(AnsiValue(178)); skin.bold.set_fg(Yellow); skin.italic.set_fg(Magenta); skin.scrollbar.thumb.set_fg(AnsiValue(178)); skin.code_block.align = Alignment::Center; skin } fn main() -> Result<(), Error> { cli_log::init_cli_log!(); let skin = make_skin(); run_app(&skin) } termimad-0.29.4/examples/high-compatibility/README.md000064400000000000000000000010471046102023000204070ustar 00000000000000This example demonstrates how to build * a default skin * a skin which would only use ASCII characters * a skin which would not use ANSI escape codes * a skin combining both properties Run this example with cargo run --example high-compatibility This adaptation to terminal capabilities is illustrated in [dysk](https://github.com/Canop/dysk): ![dysk](../../doc/dysk-nocolor-ascii.png) Most applications should also automatically disable ANSI codes when the output isn't a tty (which happens for example when the output is piped to a file). termimad-0.29.4/examples/high-compatibility/main.rs000064400000000000000000000045721046102023000204300ustar 00000000000000use { termimad::{ minimad::{ OwningTemplateExpander, TextTemplate, }, MadSkin, FmtText, terminal_size, }, }; static TEMPLATE: &str = r#" ---- # ${title} ## When to use it ? * ${points} ## Terminal capabilities: |:-:|:-:| |**Capability**|**Necessary**| |-:|:-:| |ansi escape codes|${ansi-codes}| |non ascii characters|${non-ascii}| |-:|:-:| ## Skin initialization: ``` ${code} ``` "#; fn fun( title: &str, skin: MadSkin, when: &[&str], ansi_codes: bool, non_ascii: bool, code: &str, ) { let mut expander = OwningTemplateExpander::new(); expander .set("title", title) .set_lines("points", when.join("\n")) .set_lines("code", code) .set("ansi-codes", if ansi_codes { "yes" } else { "no" }) .set("non-ascii", if non_ascii { "yes" } else { "no" }); let template = TextTemplate::from(TEMPLATE); let text = expander.expand(&template); let (width, _) = terminal_size(); let fmt_text = FmtText::from_text(&skin, text, Some(width as usize)); println!("{}", fmt_text); } fn main() { // default skin let skin = MadSkin::default(); fun( "Default skin", skin, &["almost always"], true, true, "let skin = MadSkin::default();", ); // skin without ANSI escape codes let skin = MadSkin::no_style(); fun( "Without ANSI escape codes", skin, &["when your terminal is very old"], false, true, "let skin = MadSkin::no_style();" ); // skin with only ascii chars let mut skin = MadSkin::default(); skin.limit_to_ascii(); fun( "Using only ASCII", skin, &["when your terminal only knows ASCII"], true, false, r#" let mut skin = MadSkin::default(); skin.limit_to_ascii(); "# ); // skin with only ascii chars and no ANSI escape code let mut skin = MadSkin::no_style(); skin.limit_to_ascii(); fun( "Using only ASCII and no ANSI escape code", skin, &[ "when your terminal is very very very old", "when your multiplexer is pigeon carrier based", ], false, false, r#" let mut skin = MadSkin::no_style(); skin.limit_to_ascii(); "# ); } termimad-0.29.4/examples/indented-code/main.rs000064400000000000000000000006631046102023000173410ustar 00000000000000use termimad::*; static MD: &str = r#" # Indented Code To indent code (as demonstrated here) do this: ```rust fn main() { let mut skin = MadSkin::default(); skin.code_block.left_margin = 4; skin.print_text(MD); } ``` Note that you can add some margin to other kinds of lines, not just code blocks. "#; fn main() { let mut skin = MadSkin::default(); skin.code_block.left_margin = 4; skin.print_text(MD); } termimad-0.29.4/examples/inline-template/main.rs000064400000000000000000000044331046102023000177250ustar 00000000000000/*! This example demonstrates the use of templates for building and displaying short snippets (called "inline"). You execute this example with cargo run --example inline-template */ use { std::io::Write, minimad::mad_inline, termimad::crossterm::style::{Attribute::*, Color::*}, termimad::*, }; fn main() -> Result<(), Error> { let mut skin = MadSkin::default(); skin.paragraph.set_bg(ansi(17)); skin.bold.set_fg(Yellow); skin.inline_code.add_attr(Reverse); skin.italic.set_fg(White); let mut w = std::io::stdout(); println!(); // no interpolation, just markdown mad_print_inline!(&skin, "This is *Markdown*!"); println!(); // with interpolation // Any value accepting to_string() is supported mad_print_inline!(&skin, "*count:* **$0**", 27); println!(); // another one: see that the arguments aren't interpreted as markdown, // which is convenient for user supplied texts mad_print_inline!(&skin, "**$0:** ` area = $2 ` and ` perimeter = $1 `", "Disk", "2*π*r", "π*r²"); println!(); // using any Write: mad_write_inline!(&mut w, &skin, "**$0** is *$1*", "Meow", "crazy").unwrap(); println!(); // looping: the template is compiled only once (the macro stores the compiled // template in a lazy static var) let user_supplied_strings = [ "Victor Hugo", "L'escargot et l'alouette", "Pizza weight: π * z * z * a", // the stars don't mess with the markdown ]; for (idx, string) in user_supplied_strings.iter().enumerate() { mad_print_inline!( &skin, "Exhibit $0 : *$1*", idx, string, ); println!(); } // usage of the minimad `mad_inline!` macro to get a composite allowing other operations let composite = mad_inline!( "**command:** `$0`", "cp -r /some/long/path/to/a/file /some/other/path", ); // print in a longer space and align right skin.write_composite_fill(& mut w, composite.clone(), 70, Alignment::Right).unwrap(); println!(); // print in a short span -> smart ellision occurs skin.write_composite_fill(& mut w, composite.clone(), 40, Alignment::Left).unwrap(); println!(); w.flush()?; println!(); println!(); Ok(()) } termimad-0.29.4/examples/inputs/README.md000064400000000000000000000015701046102023000161440ustar 00000000000000This example demonstrates - a responsive layout - a scrollable markdown text - a single line input field - a password input - a textarea - handling key and mouse events - managing the focus of widgets - selecting with shift-arrows - cut/copy/paste with ctrl-X, ctrl-C, ctrl-V - managing a terminal properly configured in "alternate" mode - logging events in a file (useful for event handling debugging) Run this example with cargo run --example inputs If you want to have a log file generated, run TERMIMAD_LOG=debug cargo run --example inputs Quit with ctrl-Q If you're on linux and there's a compilation error you may have to install xorg-dev and libxcb-composite0-dev, which can be done on apt based distributions with sudo apt install xorg-dev libxcb-composite0-dev termimad-0.29.4/examples/inputs/clipboard.rs000064400000000000000000000015461046102023000171750ustar 00000000000000//! The clipboard here is provided by the terminal-clipboard crate //! but you may replace it with another one use { cli_log::*, termimad::InputField, }; pub fn copy_from_input(input: &mut InputField) -> bool { let s = input.copy_selection(); if let Err(err) = terminal_clipboard::set_string(s) { warn!("error while setting clipboard: {}", err); false } else { true } } pub fn cut_from_input(input: &mut InputField) -> bool { let s = input.cut_selection(); if let Err(err) = terminal_clipboard::set_string(s) { warn!("error while setting clipboard: {}", err); false } else { true } } pub fn paste_into_input(input: &mut InputField) -> bool { if let Ok(s) = terminal_clipboard::get_string() { input.replace_selection(s); true } else { false } } termimad-0.29.4/examples/inputs/main.rs000064400000000000000000000035031046102023000161550ustar 00000000000000//! run this example with //! cargo run --example inputs //! //! if you want to have a log file generated, run //! TERMIMAD_LOG=debug cargo run --example inputs #[macro_use] extern crate cli_log; mod clipboard; mod view; use { anyhow::{self}, crokey::key, std::io::{BufWriter, stdout, Write}, termimad::*, termimad::crossterm::{ cursor, event::{ DisableMouseCapture, EnableMouseCapture, }, terminal::{ self, EnterAlternateScreen, LeaveAlternateScreen, }, QueueableCommand, }, }; fn main() -> anyhow::Result<()> { init_cli_log!(); let mut w = BufWriter::new(stdout()); w.queue(EnterAlternateScreen)?; terminal::enable_raw_mode()?; w.queue(cursor::Hide)?; w.queue(EnableMouseCapture)?; let res = run_in_alternate(&mut w); w.queue(DisableMouseCapture)?; w.queue(cursor::Show)?; // we must restore the cursor terminal::disable_raw_mode()?; w.queue(LeaveAlternateScreen)?; w.flush()?; res } /// run the event loop, in a terminal which must be in alternate fn run_in_alternate(w: &mut W) -> anyhow::Result<()> { let mut view = view::View::new(Area::full_screen()); view.queue_on(w)?; w.flush()?; info!("clipboard backend type: {}", terminal_clipboard::get_type()); let event_source = EventSource::new()?; for timed_event in event_source.receiver() { let mut quit = false; debug!("event: {:?}", timed_event); if timed_event.is_key(key!(ctrl-q)) { quit = true; } else if view.apply_timed_event(timed_event) { view.queue_on(w)?; w.flush()?; } event_source.unblock(quit); // Don't forget to unblock the event source if quit { break; } } Ok(()) } termimad-0.29.4/examples/inputs/view.rs000064400000000000000000000177101046102023000162100ustar 00000000000000use { crate::clipboard, anyhow::{self}, crokey::{key, KeyCombination}, std::io::Write, termimad::*, termimad::crossterm::{ event::{Event, MouseEvent}, queue, terminal::{ Clear, ClearType, }, }, }; /// The view covering the whole terminal, with its widgets and current state pub struct View { area: Area, drawable: bool, // is the area big enough label_skin: MadSkin, // the skin used to render the 3 labels introduction: MadView, login_label_area: Area, login_input: InputField, password_label_area: Area, password_input: InputField, comments_label_area: Area, comments_input: InputField, focus: Focus, // where is the current focus : an input or the introduction text } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Focus { Introduction, Login, Password, Comments, } impl Focus { pub fn next(self) -> Self { use Focus::*; match self { Introduction => Login, Login => Password, Password => Comments, Comments => Introduction, } } } impl Default for View { /// Create the view with all its widgets fn default() -> Self { let mut label_skin = MadSkin::default(); label_skin.paragraph.align = Alignment::Right; label_skin.headers[1].align = Alignment::Right; let mut view = Self { area: Area::uninitialized(), drawable: false, label_skin, introduction: MadView::from( MD_INTRO.to_owned(), Area::uninitialized(), MadSkin::default(), ), login_label_area: Area::uninitialized(), login_input: InputField::default(), password_label_area: Area::uninitialized(), password_input: InputField::default(), comments_label_area: Area::uninitialized(), comments_input: InputField::default(), focus: Focus::Login, }; view.login_input.set_normal_style(CompoundStyle::with_fgbg(gray(22), gray(2))); view.password_input.set_normal_style(CompoundStyle::with_fgbg(gray(22), gray(2))); view.comments_input.set_normal_style(CompoundStyle::with_fgbg(gray(22), gray(2))); view.password_input.password_mode = true; view.set_focus(Focus::Login); view.comments_input.new_line_on(InputField::ENTER); view.comments_input.set_str(MD_COMMENTS_VALUE); view } } impl View { pub fn new(area: Area) -> Self { let mut view = Self::default(); view.resize(area); view } pub fn focused_input(&mut self) -> Option<&mut InputField> { match self.focus { Focus::Login => Some(&mut self.login_input), Focus::Password => Some(&mut self.password_input), Focus::Comments => Some(&mut self.comments_input), _ => None, } } fn set_focus(&mut self, focus: Focus) { self.focus = focus; self.login_input.set_focus(focus == Focus::Login); self.password_input.set_focus(focus == Focus::Password); self.comments_input.set_focus(focus == Focus::Comments); } pub fn focus_next(&mut self) { self.set_focus(self.focus.next()); } pub fn resize(&mut self, area: Area) -> bool { if self.area == area { return false; } self.drawable = area.width >= 20 && area.height >= 15; if self.drawable { let h = 4 + (area.height - 15) / 2; let intro_area = Area::new(1, 1, area.width - 3, h); self.introduction.resize(&intro_area); let y = intro_area.bottom() + 2; let half_width = (area.width -3 ) / 2; self.login_label_area = Area::new(1, y, half_width, 1); self.login_input.change_area(half_width + 2, y, half_width); let y = y + 2; self.password_label_area = Area::new(1, y, half_width, 1); self.password_input.change_area(half_width + 2, y, half_width); let y = y + 2; let h = area.height - 1 - h; self.comments_label_area = Area::new(1, y, half_width, h); self.comments_input.set_area( Area::new(half_width + 2, y, half_width, area.height - y - 1) ); } self.area = area; true } pub fn apply_key_combination(&mut self, key: KeyCombination) -> bool { if key == key!(esc) { self.set_focus(Focus::Introduction); true } else if key == key!(tab) { self.focus_next(); true } else if let Some(input) = self.focused_input() { input.apply_key_combination(key) || { if key == key!(ctrl-c) { clipboard::copy_from_input(input) } else if key == key!(ctrl-x) { clipboard::cut_from_input(input) } else if key == key!(ctrl-v) { clipboard::paste_into_input(input) } else { false } } } else { self.introduction.apply_key_combination(key) } } pub fn apply_mouse_event(&mut self, mouse_event: MouseEvent, double_click: bool) -> bool { if self.login_input.apply_mouse_event(mouse_event, double_click) { self.set_focus(Focus::Login); } else if self.password_input.apply_mouse_event(mouse_event, double_click) { self.set_focus(Focus::Password); } else if self.comments_input.apply_mouse_event(mouse_event, double_click) { self.set_focus(Focus::Comments); } else { self.set_focus(Focus::Introduction); } true } pub fn apply_timed_event(&mut self, timed_event: TimedEvent) -> bool { match timed_event.event { Event::Key(key) => self.apply_key_combination(key.into()), Event::Mouse(me) => self.apply_mouse_event(me, timed_event.double_click), Event::Resize(w, h) => self.resize(Area::new(0, 0, w, h)), _ => false, } } /// draw the view (not flushing) pub fn queue_on(&mut self, w: &mut W) -> anyhow::Result<()> { queue!(w, Clear(ClearType::All))?; if self.drawable { let skin = &self.label_skin; self.introduction.write_on(w)?; skin.write_in_area_on(w, "## Login:", &self.login_label_area)?; self.login_input.display_on(w)?; skin.write_in_area_on(w, "## Password:", &self.password_label_area)?; self.password_input.display_on(w)?; skin.write_in_area_on(w, MD_COMMENTS_LABEL, &self.comments_label_area)?; self.comments_input.display_on(w)?; } else { self.introduction.skin.write_in_area_on( w, "*Sorry*, this terminal is **way** too small!", &self.area, )?; } Ok(()) } } static MD_INTRO: &str = r#"# Scrollable Texts and Inputs This example demonstrates scrollable texts, simple and multiline inputs, and automatically adapting to terminal resizing. - use **ctrl**-**Q** to quit the application - use the mouse or the **tab** key to give the focus to a widget - use the **page-up** and **page-down** keys to scroll multi-line inputs when necessary "#; static MD_COMMENTS_LABEL: &str = r#"## Comments: Use **enter** to add a newline (if you want to build an application using **enter** for field validation, you can ask the input to handle **alt**-**enter** as newline instead) "#; static MD_COMMENTS_VALUE: &str = r#"This is the same kind of input than the previous ones but with a higher area and allowed to create newlines. Try editing it with long sentences or scroll it or resize the terminal. Wide characters are supported: ワイド文字に対応しています。 You can also select ranges with the mouse of shift arrows, then cut, copy, paste. "#; termimad-0.29.4/examples/parse-options/main.rs000064400000000000000000000030601046102023000174340ustar 00000000000000use termimad::{ minimad, }; static MD: &str = r#" This text has too many indentations, hard-wrapping breaking an *italic part*, some code, and some **bold sub-sentence** too. To have span continue across lines, parse markdown with `minimad::parse_text( src, TreeOptions::default().continue_spans());` To fix superfluous indentations, use let options = TreeOptions::default() .clean_indentations(); "#; fn print_md(s: &str) { print_text(minimad::parse_text(s, minimad::Options::default())); } fn print_text(text: minimad::Text) { let skin = termimad::get_default_skin(); let fmt_text = termimad::FmtText::from_text(skin, text, None); println!("{fmt_text}"); } fn main() { println!(); print_md("# Raw Text:"); println!("{MD}"); // Cleaning indentations remove the indentation levels which are // most often due to the text being defined in indented raw literals println!(); print_md("# Parsed, with indentations cleaned:"); let options = minimad::Options::default() .clean_indentations(true); let text = minimad::parse_text(MD, options); print_text(text); // Span continuation allow italic, bold, strikeout, code, to // continue after a newline. // (you may be more selective, see minimad::Options) println!(); print_md("# With span continuation too:"); let options = minimad::Options::default() .clean_indentations(true) .continue_spans(true); let text = minimad::parse_text(MD, options); print_text(text); } termimad-0.29.4/examples/progress/main.rs000064400000000000000000000013441046102023000165000ustar 00000000000000/*! Run with cargo run --example progress Display the different steps of a pixel precise progress bar. Note: This example is just a draft that I'll complete later with animations, colors, etc. */ use termimad::*; fn main() { let n = 40; let mut markdown = String::new(); markdown.push_str("|-:|:-:|:-:|:-:|\n"); markdown.push_str("|iter|part|chars|bar|\n"); markdown.push_str("|-:|-|:-:|:-|\n"); for i in 0..=n { let part = (i as f32) / (n as f32); let pb = ProgressBar::new(part, 5); let char_count = pb.to_string().chars().count(); markdown.push_str(&format!("|{}|{:1.4}|{}|{}\n", i, part, char_count, pb)); } markdown.push_str("|-\n"); print_text(&markdown); } termimad-0.29.4/examples/render-input-markdown/README.md000064400000000000000000000010451046102023000210530ustar 00000000000000This example demonstrates - a responsive layout - a textarea - rendering the textarea's content as markdown Run this example with cargo run --example render-input-markdown If you want to have a log file generated, run TERMIMAD_LOG=debug cargo run --example render-input-markdown Quit with ctrl-Q If you're on linux and there's a compilation error you may have to install xorg-dev and libxcb-composite0-dev, which can be done on apt based distributions with sudo apt install xorg-dev libxcb-composite0-dev termimad-0.29.4/examples/render-input-markdown/clipboard.rs000064400000000000000000000015461046102023000221070ustar 00000000000000//! The clipboard here is provided by the terminal-clipboard crate //! but you may replace it with another one use { cli_log::*, termimad::InputField, }; pub fn copy_from_input(input: &mut InputField) -> bool { let s = input.copy_selection(); if let Err(err) = terminal_clipboard::set_string(s) { warn!("error while setting clipboard: {}", err); false } else { true } } pub fn cut_from_input(input: &mut InputField) -> bool { let s = input.cut_selection(); if let Err(err) = terminal_clipboard::set_string(s) { warn!("error while setting clipboard: {}", err); false } else { true } } pub fn paste_into_input(input: &mut InputField) -> bool { if let Ok(s) = terminal_clipboard::get_string() { input.replace_selection(s); true } else { false } } termimad-0.29.4/examples/render-input-markdown/main.rs000064400000000000000000000035421046102023000210720ustar 00000000000000//! run this example with //! cargo run --example render-input-markdown //! //! if you want to have a log file generated, run //! TERMIMAD_LOG=debug cargo run --example render-input-markdown #[macro_use] extern crate cli_log; mod clipboard; mod view; use { anyhow::{self}, crokey::key, std::io::{BufWriter, stdout, Write}, termimad::crossterm::{ cursor, event::{ DisableMouseCapture, EnableMouseCapture, }, terminal::{ self, EnterAlternateScreen, LeaveAlternateScreen, }, QueueableCommand, }, termimad::*, }; fn main() -> anyhow::Result<()> { init_cli_log!(); let mut w = BufWriter::new(stdout()); w.queue(EnterAlternateScreen)?; terminal::enable_raw_mode()?; w.queue(cursor::Hide)?; w.queue(EnableMouseCapture)?; let res = run_in_alternate(&mut w); w.queue(DisableMouseCapture)?; w.queue(cursor::Show)?; // we must restore the cursor terminal::disable_raw_mode()?; w.queue(LeaveAlternateScreen)?; w.flush()?; res } /// run the event loop, in a terminal which must be in alternate fn run_in_alternate(w: &mut W) -> anyhow::Result<()> { let mut view = view::View::new(Area::full_screen()); view.queue_on(w)?; w.flush()?; info!("clipboard backend type: {}", terminal_clipboard::get_type()); let event_source = EventSource::new()?; for timed_event in event_source.receiver() { let mut quit = false; debug!("event: {:?}", timed_event); if timed_event.is_key(key!(ctrl-q)) { quit = true; } else if view.apply_timed_event(&timed_event) { view.queue_on(w)?; w.flush()?; } event_source.unblock(quit); // Don't forget to unblock the event source if quit { break; } } Ok(()) } termimad-0.29.4/examples/render-input-markdown/view.rs000064400000000000000000000076021046102023000211210ustar 00000000000000use { crate::clipboard, anyhow::{self}, crokey::key, std::io::Write, termimad::crossterm::{ event::{Event, KeyEvent, MouseEvent}, queue, terminal::{ Clear, ClearType, }, }, termimad::*, }; /// The view covering the whole terminal, with its widgets and current state pub struct View { area: Area, drawable: bool, // is the area big enough input: InputField, render_skin: MadSkin, render_area: Area, // where the inputed markdown will be rendered } impl Default for View { /// Create the view with all its widgets fn default() -> Self { let mut render_skin = MadSkin::default(); render_skin.set_bg(gray(2)); render_skin.set_fg(ansi(230)); render_skin.set_headers_fg(ansi(222)); render_skin.bold = CompoundStyle::with_fg(ansi(194)); let mut input = InputField::default(); input.new_line_on(key!(enter)); let mut view = Self { area: Area::uninitialized(), drawable: false, input, render_skin, render_area: Area::uninitialized(), }; view.input.set_str(MD); view } } impl View { pub fn new(area: Area) -> Self { let mut view = Self::default(); view.resize(area); view } pub fn resize(&mut self, area: Area) -> bool { if self.area == area { return false; } self.drawable = area.width >= 20 && area.height >= 15; if self.drawable { let h = (area.height - 2) / 2; let input_area = Area::new(1, 1, area.width - 2, h); self.render_area = Area::new(1, h + 2, area.width -2, area.height - h - 2); self.input.set_area(input_area); } self.area = area; true } pub fn apply_key_event(&mut self, key: KeyEvent) -> bool { let input = &mut self.input; match key.into() { key!(ctrl-c) => clipboard::copy_from_input(input), key!(ctrl-x) => clipboard::cut_from_input(input), key!(ctrl-v) => clipboard::paste_into_input(input), _ => input.apply_key_event(key), } } pub fn apply_mouse_event(&mut self, mouse_event: MouseEvent, double_click: bool) -> bool { // To keep this example simple, there's no managment of focus, scrolling, etc. // See the "inputs" example for how to deal with those. // The line below allows mouse selection and wheel scrolling. self.input.apply_mouse_event(mouse_event, double_click) } pub fn apply_timed_event(&mut self, timed_event: &TimedEvent) -> bool { match timed_event.event { Event::Key(key) => self.apply_key_event(key), Event::Mouse(me) => self.apply_mouse_event(me, timed_event.double_click), Event::Resize(w, h) => self.resize(Area::new(0, 0, w, h)), _ => false, // accomodate for new crossterm events } } /// Draw the view (not flushing) pub fn queue_on(&mut self, w: &mut W) -> anyhow::Result<()> { queue!(w, Clear(ClearType::All))?; if self.drawable { self.input.display_on(w)?; let md = self.input.get_content(); let text = FmtText::from(&self.render_skin, &md, Some(self.render_area.width as usize - 1)); debug!("text: {:#?}", &text); // look at termimad.log if you want to check the parsed md let text_view = TextView::from(&self.render_area, &text); text_view.write_on(w)?; } Ok(()) } } static MD: &str = r#"# Dynamic Markdown Edit the markdown in this input and see it rendered below. You can try `code`, *italic*, **bold**, bulleted lists, tables, etc. |:-:|:-:| |shortcut|effect| |:-:|-| |**ctrl-q**| quit the example application | |**ctrl-c**| copy | |**ctrl-x**| cut | |**ctrl-v**| paste | |-|-| "#; termimad-0.29.4/examples/scrollable/main.rs000064400000000000000000000120231046102023000167520ustar 00000000000000//! run this example with //! cargo run --example scrollable //! use std::io::{stdout, Write}; use termimad::crossterm::{ cursor::{ Hide, Show}, event::{ self, Event, KeyEvent, KeyCode::*, }, queue, terminal::{ self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen, }, style::Color::*, }; use termimad::*; fn view_area() -> Area { let mut area = Area::full_screen(); area.pad_for_max_width(120); // we don't want a too wide text column area } fn run_app(skin: MadSkin) -> Result<(), Error> { let mut w = stdout(); // we could also have used stderr queue!(w, EnterAlternateScreen)?; terminal::enable_raw_mode()?; queue!(w, Hide)?; // hiding the cursor let mut view = MadView::from(MD.to_owned(), view_area(), skin); loop { view.write_on(&mut w)?; w.flush()?; match event::read() { Ok(Event::Key(KeyEvent{code, ..})) => { match code { Up => view.try_scroll_lines(-1), Down => view.try_scroll_lines(1), PageUp => view.try_scroll_pages(-1), PageDown => view.try_scroll_pages(1), _ => break, } } Ok(Event::Resize(..)) => { queue!(w, Clear(ClearType::All))?; view.resize(&view_area()); } _ => {} } } terminal::disable_raw_mode()?; queue!(w, Show)?; // we must restore the cursor queue!(w, LeaveAlternateScreen)?; w.flush()?; Ok(()) } fn make_skin() -> MadSkin { let mut skin = MadSkin::default(); skin.table.align = Alignment::Center; skin.set_headers_fg(AnsiValue(178)); skin.bold.set_fg(Yellow); skin.italic.set_fg(Magenta); skin.scrollbar.thumb.set_fg(AnsiValue(178)); skin.code_block.align = Alignment::Center; skin } fn main() -> Result<(), Error> { let skin = make_skin(); run_app(skin) } static MD: &str = r#"# Scrollable Markdown in Termimad Use the **↓** and **↑** arrow keys to scroll this page. Use any other key to quit the application. *Now I'll describe this example with more words than necessary, in order to be sure to demonstrate scrolling (and **wrapping**, too, thanks to long sentences).* ## What's shown * an **area** fitting the screen (with a max width of 120, to be prettier) * a markdown text * **parsed**, * **skinned**, * and **wrapped** to fit the width * a **scrollable** view in *raw terminal mode* ## Area The area specifies the part of the screen where we'll display our markdown. let mut area = Area::full_screen(); area.pad_for_max_width(120); // we don't want a too wide text column *(yes the code block centering in this example is a little too much, it's just here to show what's possible)* ## Parsed Markdown The text is parsed from a string. In this example we directly wrap it for the width of the area: let text = skin.area_wrapped_text(markdown, &area); If we wanted to modify the parsed representation, or modify the area width, we could also have kept the parsed text (*but parsing is cheap*). ## The TextView It's just a text put in an area, tracking your **scroll** position (and whether you want the scrollbar to be displayed). let mut text_view = TextView::from(&area, &text); ## Really Scrolling Not two applications handle events in the same way. **Termimad** doesn't try to handle this but lets you write it yourself, which is fairly easily done with **Crossterm** for example: ``` let mut events = TerminalInput::new().read_sync(); loop { text_view.write()?; if let Some(Keyboard(key)) = events.next() { match key { Up => text_view.try_scroll_lines(-1), Down => text_view.try_scroll_lines(1), PageUp => text_view.try_scroll_pages(-1), PageDown => text_view.try_scroll_pages(1), _ => break, } } } ``` ## Skin We want *shiny **colors*** (and unreasonnable centering): let mut skin = MadSkin::default(); skin.set_headers_fg(rgb(255, 187, 0)); skin.bold.set_fg(Yellow); skin.italic.set_fgbg(Magenta, rgb(30, 30, 40)); skin.scrollbar.track.set_fg(Rgb{r:30, g:30, b:40}); skin.scrollbar.thumb.set_fg(Rgb{r:67, g:51, b:0}); skin.code_block.align = Alignment::Center; The scrollbar's colors were also adjusted to be consistent. ## Usage * **↓** and **↑** arrow keys : scroll this page * any other key : quit ## And let's just finish by a table It's a little out of context but it shows how a wide table can be wrapped in a thin terminal. |feature|supported|details| |-|:-:|- | tables | yes | pipe based only, alignement not yet supported | italic, bold | yes | star based only| | inline code | yes | | code bloc | yes |with tabs. Fences not supported | crossed text | ~~not yet~~ | wait... now it works! | phpbb like links | no | (because it's preferable to show an URL in a terminal) (resize your terminal if it's too wide for wrapping to occur) "#; termimad-0.29.4/examples/serialize-skin/main.rs000064400000000000000000000031571046102023000175710ustar 00000000000000//! this example prints a skin as JSON to stdout. //! //! Run it with `cargo run --example serialize-skin` use { termimad::{ ansi, gray, ROUNDED_TABLE_BORDER_CHARS, rgb, StyledChar, minimad::Alignment, crossterm::style::{ Attribute, Color::*, }, }, }; /// Set this to true if you want to see a skin with a lot of changes /// from the default, set it to false to export the default skin const FANCY: bool = true; fn main() { let mut skin = termimad::MadSkin::default(); if FANCY { skin.set_headers_fg(AnsiValue(178)); skin.headers[0].set_bg(ansi(129)); skin.headers[1].add_attr(Attribute::Bold); // note: gray(22) will appear as ansi(254) in the output, // it's the same color skin.headers[2].set_fg(gray(22)); skin.bold.set_fg(Yellow); skin.italic.set_fgbg(Magenta, rgb(30, 30, 40)); skin.bullet = StyledChar::from_fg_char(Yellow, '⟡'); skin.quote_mark.set_fg(Yellow); skin.italic.set_fg(Magenta); skin.scrollbar.thumb.set_fg(AnsiValue(178)); skin.table_border_chars = ROUNDED_TABLE_BORDER_CHARS; skin.paragraph.align = Alignment::Center; skin.table.align = Alignment::Center; skin.table.right_margin = 4; skin.inline_code.add_attr(Attribute::Reverse); skin.paragraph.set_fg(Magenta); skin.italic.add_attr(Attribute::Underlined); skin.italic.add_attr(Attribute::OverLined); } let serialized = serde_json::to_string_pretty(&skin).unwrap(); println!("{serialized}"); } termimad-0.29.4/examples/serialize-skin/skin.hjson000064400000000000000000000006171046102023000203040ustar 00000000000000# This Hjson file is an example skin. # You can modify it then run `cargo run --example skin-file` bold: "#fb0 bold" italic: dim italic strikeout: crossedout red bullet: ○ yellow bold paragraph: gray(20) code_block: gray(2) gray(15) center headers: [ yellow bold center yellow underlined yellow ] quote: > red horizontal-rule: "~ #00cafe" table: "#540 center" scrollbar: "red yellow" termimad-0.29.4/examples/simple/main.rs000064400000000000000000000020341046102023000161220ustar 00000000000000use termimad::crossterm::style::{Attribute::*, Color::*}; use termimad::*; fn show(skin: &MadSkin, src: &str) { println!(" Raw : {}", &src); println!(" Formatted : {}\n", skin.inline(src)); } fn show_some(skin: &MadSkin) { show(skin, "*Hey* **World!** Here's `some(code)`"); show(skin, "some *nested **style***"); } fn main() { println!(); println!("\nWith the default skin:\n"); let mut skin = MadSkin::default(); show_some(&skin); println!("\nWith a customized skin:\n"); skin.bold.set_fg(Yellow); skin.italic = CompoundStyle::with_bg(DarkBlue); skin.inline_code.add_attr(Reverse); show_some(&skin); let mut skin = MadSkin::default(); skin.bold.set_fg(Yellow); skin.print_inline("*Hey* **World!** Here's `some(code)`"); skin.paragraph.set_fgbg(Magenta, rgb(30, 30, 40)); skin.italic.add_attr(Underlined); skin.italic.add_attr(OverLined); println!( "\nand now {}\n", skin.inline("a little *too much* **style!** (and `some(code)` too)") ); } termimad-0.29.4/examples/skin-file/main.rs000064400000000000000000000102631046102023000165150ustar 00000000000000use { std::fs, termimad::{ minimad::{ OwningTemplateExpander, TextTemplate, }, MadSkin, FmtText, terminal_size, }, }; static TEMPLATE: &str = r#" ---- # Skin File ## What this example demonstrates * read a skin from the skin file in the example directory * print this text with the skin just deserialized * a simple template * ~~animated style transitions~~ ## How it works The skin file is read into a string, then **deserialized** into a `MadSkin`: ``` let hjson = fs::read_to_string(file_path)?; let skin: MadSkin = deser_hjson::from_str(&hjson)?; ``` *(of course it doesn't have to be Hjson, it can be JSON, TOML, or any serde compatible format of your choice)* As we want to print the real skin file, we use a template (see the complete code), but using the skin could have been as simple as ``` skin.print_text(MD); ``` In your own program, you may want to not parse a `MadSkin` but some specific styling parts then build the skin(s) yourself. In this case, use the various functions of the `parse` module: they allow parsing `Color`, `LineStyle`, `CompoundStyle`, `StyledChar`, etc. from strings using the same syntax than for a whole skin deserialization. ## Skin syntax Here's the content of the skin file used to render this text: ${skin} ### Colors: A color can be described as * one of the standard ANSI color names: `red`, `yellow`, `magenta`, etc. * a gray level, `gray(0)` (dark) to `gray(23)` (light) * an ANSI color code, eg `ansi(123)` * a rgb color, eg `rgb(255, 0, 50)`, `#fb0`, or `#cafe00` ### Inline styles: Inline styles are "bold", "italic", "strikeout", "inline-code", and "ellipsis". They're defined by a foreground color, a background color, and attributes, all those parts being optional. The first encountered color is the foreground color. If you want no foreground color but a background one, use `none`, eg `bold none red`. ### Line styles Line styles are "paragraph", "code-block", and "table". They're defined like inline styles but accept an optional alignment (`left`, `right`, or `center`) and optional left and right margins. ### Styled chars Styled chars are "bullet", "quote", and "horizontal-rule". They're defined by a character (which must be one character wide and long), and foreground and background colors. All parts are optional. ### Scrolled bar It's defined either by * a char, a fg color and a bg color, all parts optional * a struct with two styled chars named `track` and `thumb` Examples: ```Hjson scrollbar: white ``` ```Hjson scrollbar: { track: | darkblue black thumb: | lightblue black bold } ``` ### Headers: Headers are line styles too. They can he defined one per one: headers: [ yellow bold center yellow underlined yellow ] *(you don't have to define all 8 possible levels, others will stay default)* It's also possible to change all default headers with a shortened syntax. The example below would set all headers to be in yellow and italic but keep all other properties as default: headers: yellow italic ### Summary: Skin entries |:-:|:-:|:-:| |**keys**|**type**|**comments**| |:-:|:-:|:-| |bold|inline| |italic|inline| |strikeout|inline| |inline-code, inline_code|inline| |paragraph|line|standard line |code-block, code_block|line| |table|line|fg and bg colors are for the border |bullet|character| |quote, quote-mark, quote_mark|character| |scrollbar|character| |horizontal-rule, horizontal_rule, rule|character| |:-:|:-:|:-| ---- "#; fn main() { // read the skin file in a string let hjson = fs::read_to_string("examples/skin-file/skin.hjson").unwrap(); // deserialize the Hjson into a skin let skin: MadSkin = deser_hjson::from_str(&hjson).unwrap(); // build the text with a template so that we can include // the real content of the skin file let mut expander = OwningTemplateExpander::new(); expander.set_lines("skin", hjson); let template = TextTemplate::from(TEMPLATE); let text = expander.expand(&template); // render the text in the terminal let (width, _) = terminal_size(); let fmt_text = FmtText::from_text(&skin, text, Some(width as usize)); print!("{}", fmt_text); } termimad-0.29.4/examples/skin-file/skin.hjson000064400000000000000000000006161046102023000172330ustar 00000000000000# This Hjson file is an example skin. # You can modify it then run `cargo run --example skin-file` bold: "#fb0 bold" italic: dim italic strikeout: crossedout red bullet: ○ yellow bold paragraph: gray(20) 4 4 code_block: gray(2) gray(15) 4 headers: [ yellow bold center yellow underlined yellow ] quote: > red horizontal-rule: "~ #00cafe" table: "#540 center" scrollbar: "red yellow" termimad-0.29.4/examples/skin-file/skin.json000064400000000000000000000013531046102023000170620ustar 00000000000000{ "bold": "Yellow Bold", "italic": "Magenta rgb(30, 30, 40) Italic Underlined OverLined", "strikeout": "CrossedOut", "inline_code": "ansi(249) ansi(235) Reverse", "ellipsis": "", "bullet": "⟡ Yellow", "quote": "▐ Yellow Bold", "horizontal_rule": "― ansi(238)", "scrollbar": "▐ ansi(178) ansi(237)", "paragraph": "Magenta center 4 4", "code_block": "ansi(249) ansi(235) 4", "table": "ansi(239) center", "headers": [ "ansi(178) ansi(129) Bold Underlined center", "ansi(178) Bold Underlined ", "ansi(254) Underlined ", "ansi(178) Underlined ", "ansi(178) Underlined ", "ansi(178) Underlined ", "ansi(178) Underlined ", "ansi(178) Underlined " ], "table_border_chars": "rounded" } termimad-0.29.4/examples/stderr/main.rs000064400000000000000000000052431046102023000161410ustar 00000000000000//! This example demonstrates how to run an application on stderr //! //! Run this example with //! cargo run --example stderr //! use termimad::crossterm::{ event::{ self, Event, KeyEvent, KeyCode::*, }, cursor, queue, terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, style::Color::*, }; use termimad::*; const MARKDOWN: &str = r#" This screen is ran on *stderr*. And when you quit it, 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 --example stderr and then you run it with cd "$(target/debug/examples/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 `~` * or anything else to print this text (so that you may copy-paste) "#; fn run_app(skin: MadSkin, w: &mut W) -> Result, Error> where W: std::io::Write, { queue!(w, EnterAlternateScreen)?; terminal::enable_raw_mode()?; queue!(w, cursor::Hide)?; // hiding the cursor let mut area = Area::full_screen(); area.pad(1, 1); // let's add some margin area.pad_for_max_width(120); // we don't want a too wide text column let mut view = MadView::from(MARKDOWN.to_owned(), area, skin); let mut user_char = None; loop { view.write_on(w)?; w.flush()?; if let Ok(Event::Key(KeyEvent{code, modifiers, ..})) = event::read() { if !modifiers.is_empty() { continue; } match code { Up => view.try_scroll_lines(-1), Down => view.try_scroll_lines(1), PageUp => view.try_scroll_pages(-1), PageDown => view.try_scroll_pages(1), Char(c) => { user_char = Some(c); break; } _ => break, } } } terminal::disable_raw_mode()?; queue!(w, cursor::Show)?; // we must restore the cursor queue!(w, LeaveAlternateScreen)?; w.flush()?; Ok(user_char) } fn main() { let mut skin = MadSkin::default(); skin.set_headers_fg(AnsiValue(178)); skin.bold.set_fg(Yellow); skin.italic.set_fg(Magenta); skin.scrollbar.thumb.set_fg(AnsiValue(178)); let mut stderr = std::io::stderr(); match run_app(skin.clone(), &mut stderr).unwrap() { Some('1') => print!(".."), Some('2') => print!("/"), Some('3') => print!("~"), _ => skin.print_text(MARKDOWN), } } termimad-0.29.4/examples/table/main.rs000064400000000000000000000023431046102023000157230ustar 00000000000000use termimad::crossterm::style::Color::*; use termimad::*; static MD_TABLE: &str = r#" |:-:|:-:|- |**feature**|**supported**|**details**| |-:|:-:|- | tables | yes | pipe based, with or without alignments | italic, bold | yes | star based | | inline code | yes | `with backquotes` (it works in tables too) | code bloc | yes |with tabs or code fences | syntax coloring | no | | crossed text | ~~not yet~~ | wait... now it works `~~like this~~` | horizontal rule | yes | Use 3 or more dashes (`---`) | lists | yes|* unordered lists supported | | |* ordered lists *not* supported | quotes | yes |> What a wonderful time to be alive! | links | no | (but your terminal already handles raw URLs) |- *Run this example again in a terminal with a different width* "#; fn main() { println!("\n"); let mut skin = MadSkin::default(); skin.set_headers_fg(rgb(255, 187, 0)); skin.bold.set_fg(Yellow); skin.italic.set_fgbg(Magenta, rgb(30, 30, 40)); skin.paragraph.align = Alignment::Center; skin.table.align = Alignment::Center; let (width, _) = terminal_size(); let mut markdown = format!("Available width: *{}*", width); markdown.push_str(MD_TABLE); println!("{}", skin.term_text(&markdown)); println!("\n"); } termimad-0.29.4/examples/text/main.rs000064400000000000000000000037461046102023000156300ustar 00000000000000use termimad::crossterm::{execute, style::Color::*, terminal}; use termimad::*; static MD: &str = r#" ---- # Markdown Rendering on Terminal Here's the code to print this markdown block in the terminal: ```rust let mut skin = MadSkin::default(); skin.set_headers_fg(rgb(255, 187, 0)); skin.bold.set_fg(Yellow); skin.italic.set_fgbg(Magenta, rgb(30, 30, 40)); skin.bullet = StyledChar::from_fg_char(Yellow, '⟡'); skin.quote_mark.set_fg(Yellow); println!("{}", skin.term_text(my_markdown)); ``` **Termimad** is built over **Crossterm** and **Minimad**. ---- ## Why use Termimad * *display* static or dynamic *rich* texts * *separate* your text building code or resources from its styling * *configure* your colors ## Real use cases * the help screen of a terminal application * small snippets of rich text in a bigger application * terminal app output ## What people say about Termimad > I find it convenient *[Termimad's author]* ---- "#; fn print_direct(skin: &MadSkin) { skin.print_text(MD); } fn print_in_text_view(skin: MadSkin) { let mut w = std::io::stdout(); execute!(w, terminal::Clear(terminal::ClearType::All)).unwrap(); let mut area = Area::full_screen(); area.pad(2, 1); // let's add some margin let text = skin.area_text(MD, &area); let view = TextView::from(&area, &text); view.write().unwrap(); } /// Choose DIRECT = true for a simple writting in stdout, /// and DIRECT = false for a whole terminal display. /// Note that this doesn't use an alternate screen. Look /// at the "scrollable" example to see an alternate screen /// being used. const DIRECT: bool = true; fn main() { let mut skin = MadSkin::default(); skin.set_headers_fg(rgb(255, 187, 0)); skin.bold.set_fg(Yellow); skin.italic.set_fgbg(Magenta, rgb(30, 30, 40)); skin.bullet = StyledChar::from_fg_char(Yellow, '⟡'); skin.quote_mark.set_fg(Yellow); if DIRECT { print_direct(&skin); } else { print_in_text_view(skin); println!(); } } termimad-0.29.4/examples/text-template/main.rs000064400000000000000000000054141046102023000174330ustar 00000000000000/*! This example demonstrates the use of templates for building whole texts. You execute this example with cargo run --example text-template */ use { minimad::{TextTemplate, OwningTemplateExpander}, termimad::crossterm::style::Color::*, termimad::*, }; static TEMPLATE: &str = r#" ----------- # ${app-name} v${app-version} **${app-name}** is *fantastic*! ## Modules in a table |:-:|:-:|:-:| |**name**|**path**|**count**| |-:|:-:|:-:| ${module-rows |**${module-name}**|`${app-version}/${module-key}`|${module-count}| } |-|-|-| ## Modules again (but with a different presentations): ${module-rows **${module-name}** (*${module-key}*): count: ${module-count} ${module-description} } ## Example of a code block ${some-function} ## Fenced Code block with a placeholder ```rust this_is_some(code); this_part.is(${dynamic}); ``` That's all for now. ----------- "#; /// a struct to illustrate several ways to format its information struct Module { name: &'static str, key: &'static str, count: u64, description: &'static str, } /// some example data const MODULES: &[Module] = &[ Module { name: "lazy-regex", key: "lrex", count: 0, description: "eases regexes"}, Module { name: "termimad", key: "tmd", count: 7, description: "do things on *terminal*" }, Module { name: "bet", key: "bet", count: 11, description: "do formulas, unlike `S=π*r²`" }, Module { name: "umask", key: "mod", count: 2, description: "my mask" }, ]; fn main() -> Result<(), Error> { // fill an expander with data let mut expander = OwningTemplateExpander::new(); expander .set("app-name", "MyApp") .set("app-version", "42.5.3") .set_md("dynamic", "filled_by_**template**"); // works in code too for module in MODULES { expander.sub("module-rows") .set("module-name", module.name) .set("module-key", module.key) .set("module-count", format!("{}", module.count)) .set_md("module-description", module.description); } expander.set_lines("some-function", r#" fun test(a rational) { irate(a) } "#); // use the data to build the markdown text and print it let skin = make_skin(); let template = TextTemplate::from(TEMPLATE); let text = expander.expand(&template); let (width, _) = terminal_size(); let fmt_text = FmtText::from_text(&skin, text, Some(width as usize)); print!("{}", fmt_text); Ok(()) } fn make_skin() -> MadSkin { let mut skin = MadSkin::default(); skin.set_headers_fg(AnsiValue(178)); skin.headers[2].set_fg(gray(22)); skin.bold.set_fg(Yellow); skin.italic.set_fg(Magenta); skin.scrollbar.thumb.set_fg(AnsiValue(178)); skin.table_border_chars = ROUNDED_TABLE_BORDER_CHARS; skin } termimad-0.29.4/fit-width-example.sh000075500000000000000000000001161046102023000154070ustar 00000000000000#!/bin/bash RUST_BACKTRACE=1 TERMIMAD_LOG=debug cargo run --example fit-width termimad-0.29.4/inputs-example.sh000075500000000000000000000001131046102023000150270ustar 00000000000000#!/bin/bash RUST_BACKTRACE=1 TERMIMAD_LOG=debug cargo run --example inputs termimad-0.29.4/src/area.rs000064400000000000000000000116151046102023000135730ustar 00000000000000use { crate::crossterm::terminal, std::convert::{TryFrom, TryInto}, }; /// A default width which is used when we failed measuring the real terminal width const DEFAULT_TERMINAL_WIDTH: u16 = 50; /// A default height which is used when we failed measuring the real terminal width const DEFAULT_TERMINAL_HEIGHT: u16 = 20; /// A rectangular part of the screen #[derive(Debug, PartialEq, Eq, Clone)] pub struct Area { pub left: u16, pub top: u16, pub width: u16, pub height: u16, } impl Default for Area { fn default() -> Self { Self::uninitialized() } } impl Area { /// build a new area. You'll need to set the position and size /// before you can use it pub const fn uninitialized() -> Area { Area { left: 0, top: 0, height: 1, width: 5, } } /// build a new area. pub const fn new(left: u16, top: u16, width: u16, height: u16) -> Area { Area { left, top, width, height, } } /// build an area covering the whole terminal pub fn full_screen() -> Area { let (width, height) = terminal_size(); Area { left: 0, top: 0, width, height, } } pub const fn right(&self) -> u16 { self.left + self.width } pub const fn bottom(&self) -> u16 { self.top + self.height } /// tell whether the char at (x,y) is in the area pub const fn contains(&self, x: u16, y: u16) -> bool { x >= self.left && x < self.left + self.width && y >= self.top && y < self.top + self.height } /// shrink the area pub fn pad(&mut self, dx: u16, dy: u16) { // this will crash if padding is too big. feature? self.left += dx; self.top += dy; self.width -= 2 * dx; self.height -= 2 * dy; } /// symmetrically shrink the area if its width is bigger than `max_width` pub fn pad_for_max_width(&mut self, max_width: u16) { if max_width >= self.width { return; } let pw = self.width - max_width; self.left += pw / 2; self.width -= pw; } /// Return an option which when filled contains /// a tupple with the top and bottom of the vertical /// scrollbar. Return none when the content fits /// the available space. pub fn scrollbar( &self, scroll: U, // number of lines hidden on top content_height: U, ) -> Option<(u16, u16)> where U: Into { compute_scrollbar(scroll, content_height, self.height, self.top) } } /// Compute the min and max y (from the top of the terminal, both inclusive) /// for the thumb part of the scrollbar which would represent the scrolled /// content in the available height. /// /// If you represent some data in an Area, you should directly use the /// scrollbar method of Area. pub fn compute_scrollbar( scroll: U1, // 0 for no scroll, positive if scrolled content_height: U1, // number of lines of the content available_height: U3, // for an area it's usually its height top: U2, // distance from the top of the screen ) -> Option<(U2, U2)> where U1: Into, // the type in which you store your content length and content scroll U2: Into + TryFrom, // the drawing type (u16 for an area) >::Error: std::fmt::Debug, U3: Into + TryFrom, // the type used for available height >::Error: std::fmt::Debug, { let scroll: usize = scroll.into(); let content_height: usize = content_height.into(); let available_height: usize = available_height.into(); let top: usize = top.into(); if content_height <= available_height { return None; } let mut track_before = scroll * available_height / content_height; if track_before == 0 && scroll > 0 { track_before = 1; } let thumb_height = available_height * available_height / content_height; let scrollbar_top = top + track_before; let mut scrollbar_bottom = scrollbar_top + thumb_height; if scroll + available_height < content_height && available_height > 3 { scrollbar_bottom = scrollbar_bottom .min(top + available_height - 2) .max(scrollbar_top); } // by construction those two conversions are OK // (or it's a bug, which, well, is possible...) let scrollbar_top = scrollbar_top.try_into().unwrap(); let scrollbar_bottom = scrollbar_bottom.try_into().unwrap(); Some((scrollbar_top, scrollbar_bottom)) } /// Return a (width, height) with the dimensions of the available /// terminal in characters. /// pub fn terminal_size() -> (u16, u16) { let size = terminal::size(); size.unwrap_or((DEFAULT_TERMINAL_WIDTH, DEFAULT_TERMINAL_HEIGHT)) } termimad-0.29.4/src/ask.rs000064400000000000000000000135151046102023000134420ustar 00000000000000use { crate::*, std::io, }; /// a question that can be asked to the user, requiring /// him to type the key of the desired answer /// /// A question can be built using [Question::new] or with /// the [ask!] macro pub struct Question { pub md: Option, pub answers: Vec, pub default_answer: Option, } /// one of the proposed answers to a question pub struct Answer { pub key: String, pub md: String, } impl Question { /// Create a new question with some text. pub fn new>(md: S) -> Self { Self { md: Some(md.into()), answers: Vec::new(), default_answer: None, } } /// add a proposed answer, with a key /// /// The user will have to type the result of calling `to_string()` on /// the key (numbers, chars, or strings are naturally good options for keys) pub fn add_answer>(&mut self, key: K, md: S) { self.answers.push(Answer { key: key.to_string(), md: md.into(), }); } /// set the value which will be returned if the user only hits enter. /// /// It does *not* have to be one of the answers' key, except when you /// use the [ask!] macro. pub fn set_default(&mut self, default_answer: K) { self.default_answer = Some(default_answer.to_string()); } /// has a default been defined which isn't among the list of answers? pub fn has_exotic_default(&self) -> bool { if let Some(da) = self.default_answer.as_ref() { for answer in &self.answers { if &answer.key == da { return false; } } true } else { false } } /// Does the asking and returns the inputted string, unless /// the user just typed *enter* and there was a default value. /// /// If the user types something not recognized, he's asking to /// try again. pub fn ask(&self, skin: &MadSkin) -> io::Result { if let Some(md) = &self.md { skin.print_text(md); } for a in &self.answers { if self.default_answer.as_ref() == Some(&a.key) { mad_print_inline!(skin, "[**$0**] ", a.key); } else { mad_print_inline!(skin, "[$0] ", a.key); } skin.print_text(&a.md); } loop { let mut input = String::new(); io::stdin().read_line(&mut input)?; input.truncate(input.trim_end().len()); if input.is_empty() { if let Some(da) = &self.default_answer { return Ok(da.clone()); } } for a in &self.answers { if a.key == input { return Ok(input); } } println!("answer {:?} not understood", input); } } } /// ask the user to choose among proposed answers. /// /// This macro makes it possible to propose several choices, with /// an optional default one, to execute blocks, to optionaly return /// a value. /// /// Example of a simple confirmation:: /// /// ```no_run /// let confirmed = termimad::ask!( /// termimad::get_default_skin(), /// "Do you want to erase the disk ?", ('n') { /// ('y', "**Y**es") => { true } /// ('n', "**N**o") => { false } /// } /// ); /// ``` /// /// Example of chained questions: /// /// ```no_run /// use termimad::*; /// /// let skin = get_default_skin(); /// let beverage = ask!(skin, "What do I serve you ?", { /// ('b', "**B**eer") => { /// ask!(skin, "Really ? We have wine and orange juice too", (2) { /// ("oj", "**o**range **j**uice") => { "orange juice" } /// ('w' , "ok for some wine") => { "wine" } /// ('b' , "I said **beer**") => { "beer" } /// ( 2 , "Make it **2** beer glasses!") => { "beer x 2" } /// }) /// } /// ('w', "**W**ine") => { /// println!("An excellent choice!"); /// "wine" /// } /// }); /// ``` /// /// Limits compared to the [Question] API: /// - the default answer, if any, must be among the declared ones /// /// Note that examples/ask contains several examples of this macro. #[macro_export] macro_rules! ask { ( $skin: expr, $question: expr, { $(($key: expr, $answer: expr) => $r: block)+ } ) => {{ let mut question = $crate::Question { md: Some($question.to_string()), answers: vec![$($crate::Answer { key: $key.to_string(), md: $answer.to_string() }),*], default_answer: None, }; let key = question.ask($skin).unwrap(); let mut answers = question.answers.drain(..); match key { $( _ if answers.next().unwrap().key == key => { $r } )* _ => { unreachable!(); } } }}; ( $skin: expr, $question: expr, ($default_answer: expr) { $(($key: expr, $answer: expr) => $r: block)+ } ) => {{ let mut question = $crate::Question { md: Some($question.to_string()), answers: vec![$($crate::Answer { key: $key.to_string(), md: $answer.to_string() }),*], default_answer: Some($default_answer.to_string()), }; if question.has_exotic_default() { // I should rewrite this macro as a proc macro... panic!("default answer when using the ask! macro must be among declared answers"); } let key = question.ask($skin).unwrap(); let mut answers = question.answers.drain(..); match key { $( _ if answers.next().unwrap().key == key => { $r } )* _ => { unreachable!() } } }} } termimad-0.29.4/src/code.rs000064400000000000000000000042601046102023000135730ustar 00000000000000use { crate::*, minimad::Alignment, }; /// a sequence of lines whose line-style is Code #[derive(Debug)] pub struct CodeBlock { pub start: usize, pub height: usize, // number of lines pub width: usize, // length in chars of the widest line } impl CodeBlock { /// ensure all lines of the block have the same width pub fn justify(&self, lines: &mut [FmtLine<'_>]) { for line in lines.iter_mut().skip(self.start).take(self.height) { if let FmtLine::Normal(ref mut fc) = line { fc.spacing = Some(Spacing { width: self.width, align: Alignment::Left, }); } } } } const fn code_line_length(line: &FmtLine<'_>) -> Option { match line { FmtLine::Normal(fc) => match fc.kind { CompositeKind::Code => Some(fc.visible_length), _ => None, }, _ => None, } } /// find ranges of code lines in a text. /// /// Warning: the indices in a codeblock are invalid as /// soon as lines are inserted or removed. This function /// should normally not be used from another module or lib pub fn find_blocks(lines: &[FmtLine<'_>]) -> Vec { let mut blocks: Vec = Vec::new(); let mut current: Option = None; for (idx, line) in lines.iter().enumerate() { if let Some(ll) = code_line_length(line) { match current.as_mut() { Some(b) => { b.height += 1; b.width = b.width.max(ll); } None => { current = Some(CodeBlock { start: idx, height: 1, width: ll, }); } } } else if let Some(c) = current.take() { blocks.push(c); } } if let Some(c) = current.take() { blocks.push(c); } blocks } /// ensure the widths of all lines in a code block are /// the same line. pub fn justify_blocks(lines: &mut [FmtLine<'_>]) { let blocks = find_blocks(lines); for b in blocks { b.justify(lines); } } termimad-0.29.4/src/color.rs000064400000000000000000000010221046102023000137700ustar 00000000000000use { crate::crossterm::style::Color, }; /// Build a RGB color /// /// ``` /// let gold = termimad::rgb(255, 187, 0); /// ``` pub const fn rgb(r: u8, g: u8, b: u8) -> Color { Color::Rgb { r, g, b } } /// Build a gray-level color, from 0 (mostly dark) to 23 (light). pub fn gray(mut level: u8) -> Color { level = level.min(23); Color::AnsiValue(0xE8 + level) } /// Build an [ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit) pub const fn ansi(level: u8) -> Color { Color::AnsiValue(level) } termimad-0.29.4/src/composite.rs000064400000000000000000000070651046102023000146710ustar 00000000000000use { crate::*, minimad::{Composite, Compound}, unicode_width::UnicodeWidthStr, }; /// Wrap Minimad compounds with their style and /// termimad specific information #[derive(Debug, Clone)] pub struct FmtComposite<'s> { pub kind: CompositeKind, pub compounds: Vec>, // cached visible length in cells, not counting margins, bullets, etc. pub visible_length: usize, pub spacing: Option, } impl<'s> FmtComposite<'s> { pub fn new() -> Self { FmtComposite { kind: CompositeKind::Paragraph, compounds: Vec::new(), visible_length: 0, spacing: None, } } pub fn from(composite: Composite<'s>, skin: &MadSkin) -> Self { let kind: CompositeKind = composite.style.into(); FmtComposite { visible_length: skin.visible_composite_length(kind, &composite.compounds), kind, compounds: composite.compounds, spacing: None, } } pub fn from_compound(compound: Compound<'s>) -> Self { let mut fc = Self::new(); fc.add_compound(compound); fc } /// Return the number of characters (usually spaces) to insert both /// sides of the composite #[inline(always)] pub const fn completions(&self) -> (usize, usize) { match &self.spacing { Some(spacing) => spacing.completions_for(self.visible_length), None => (0, 0), } } /// Add a compound and modifies `visible_length` accordingly #[inline(always)] pub fn add_compound(&mut self, compound: Compound<'s>) { self.visible_length += compound.src.width(); self.compounds.push(compound); } /// Ensure the cached visible_length is correct. /// /// It's normally not necessary to call it, but /// this must be called if compounds are added, /// removed or modified without using the FmtComposite API pub fn recompute_width(&mut self, skin: &MadSkin) { self.visible_length = skin.visible_composite_length(self.kind, &self.compounds); } /// try to ensure the composite's width doesn't exceed the given /// width. /// /// The alignment can be used, if necessary, to know which side it's better /// to remove content (for example if the alignment is left then we remove at /// right). /// The fitter may remove a part in the core of the composite if it looks /// good enough. In this specific case an ellipsis will replace the removed part. pub fn fit_width(&mut self, width: usize, align: Alignment, skin: &MadSkin) { Fitter::for_align(align).fit(self, width, skin); } /// if the composite is smaller than the given width, pad it /// according to the alignment. pub fn extend_width(&mut self, width: usize, align: Alignment) { if let Some(ref mut spacing) = self.spacing { if spacing.width < width { spacing.width = width; } spacing.align = align; } else if self.visible_length < width { self.spacing = Some(Spacing { width, align }); } } /// try to make it so that the composite has exactly the given width, /// either by shortening it or by adding space. /// /// This calls the `fit_width` and `extend_width` methods. pub fn fill_width(&mut self, width: usize, align: Alignment, skin: &MadSkin) { self.fit_width(width, align, skin); self.extend_width(width, align); } } impl Default for FmtComposite<'_> { fn default() -> Self { Self::new() } } termimad-0.29.4/src/composite_kind.rs000064400000000000000000000012261046102023000156670ustar 00000000000000use { crate::*, minimad::*, }; /// The global kind of a composite #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum CompositeKind { Paragraph, Header(u8), ListItem(u8), ListItemFollowUp(u8), Code, Quote, } impl From for CompositeKind { fn from(ty: CompositeStyle) -> Self { match ty { CompositeStyle::Paragraph => Self::Paragraph, CompositeStyle::Header(level) => Self::Header(level), CompositeStyle::ListItem(level) => Self::ListItem(level), CompositeStyle::Code => Self::Code, CompositeStyle::Quote => Self::Quote, } } } termimad-0.29.4/src/compound_style.rs000064400000000000000000000175021046102023000157300ustar 00000000000000use { crate::{ errors::Result, crossterm::{ QueueableCommand, style::{ Attribute, Attributes, Color, ContentStyle, PrintStyledContent, SetBackgroundColor, SetForegroundColor, StyledContent, }, terminal::{Clear, ClearType}, }, styled_char::StyledChar, }, std::fmt::{self, Display}, }; /// The attributes which are often supported pub static ATTRIBUTES: &[Attribute] = &[ Attribute::Bold, Attribute::Dim, Attribute::Italic, Attribute::Underlined, Attribute::Reverse, Attribute::CrossedOut, Attribute::Encircled, Attribute::OverLined, ]; /// A style which may be applied to a compound #[derive(Default, Clone, Debug, PartialEq)] pub struct CompoundStyle { pub object_style: ContentStyle, // a crossterm content style } impl From for CompoundStyle { fn from(object_style: ContentStyle) -> Self { Self { object_style } } } impl CompoundStyle { /// Apply an `StyledContent` to the passed displayable object. pub fn apply_to(&self, val: D) -> StyledContent where D: Clone + Display, { self.object_style.apply(val) } /// Get an new instance of `CompoundStyle` pub const fn new( foreground_color: Option, background_color: Option, attributes: Attributes, ) -> Self { Self { object_style: ContentStyle { foreground_color, background_color, underline_color: None, attributes, }, } } /// Blend the foreground and background colors (if any) into the given dest color, /// with a weight in `[0..1]`. /// /// The `dest` color can be for example a [crossterm] color or a [coolor] one. pub fn blend_with>(&mut self, dest: C, weight: f32) { debug_assert!(weight>=0.0 && weight<=1.0); let dest: coolor::Color = dest.into(); if let Some(fg) = self.object_style.foreground_color.as_mut() { let src: coolor::Color = (*fg).into(); *fg = coolor::Color::blend(src, 1.0 - weight, dest, weight).into(); } if let Some(bg) = self.object_style.foreground_color.as_mut() { let src: coolor::Color = (*bg).into(); *bg = coolor::Color::blend(src, 1.0 - weight, dest, weight).into(); } } /// Get an new instance of `CompoundStyle` pub fn with_fgbg(fg: Color, bg: Color) -> Self { Self::new( Some(fg), Some(bg), Attributes::default(), ) } /// Get an new instance of `CompoundStyle` pub fn with_fg(fg: Color) -> Self { Self::new( Some(fg), None, Attributes::default(), ) } /// Get an new instance of `CompoundStyle` pub fn with_bg(bg: Color) -> Self { Self::new( None, Some(bg), Attributes::default(), ) } /// Get an new instance of `CompoundStyle` pub fn with_attr(attr: Attribute) -> Self { let mut cp = Self::default(); cp.add_attr(attr); cp } /// Set the foreground color to the passed color. pub fn set_fg(&mut self, color: Color) { self.object_style.foreground_color = Some(color); } /// Set the background color to the passed color. pub fn set_bg(&mut self, color: Color) { self.object_style.background_color = Some(color); } /// Set the colors to the passed ones pub fn set_fgbg(&mut self, fg: Color, bg: Color) { self.object_style.foreground_color = Some(fg); self.object_style.background_color = Some(bg); } /// Add an `Attribute`. Like italic, underlined or bold. pub fn add_attr(&mut self, attr: Attribute) { self.object_style.attributes.set(attr); } /// Check whether the style contains the attribute pub fn has_attr(&self, attr: Attribute) -> bool { self.object_style.attributes.has(attr) } /// Remove an `Attribute`. Like italic, underlined or bold. pub fn remove_attr(&mut self, attr: Attribute) { self.object_style.attributes.unset(attr); } /// Add the defined characteristics of `other` to self, overwriting /// its own one when defined pub fn overwrite_with(&mut self, other: &Self) { self.object_style.foreground_color = other .object_style .foreground_color .or(self.object_style.foreground_color); self.object_style.background_color = other .object_style .background_color .or(self.object_style.background_color); self.object_style .attributes .extend(other.object_style.attributes); } #[inline(always)] pub const fn get_fg(&self) -> Option { self.object_style.foreground_color } #[inline(always)] pub const fn get_bg(&self) -> Option { self.object_style.background_color } /// Write a char several times with the line compound style #[inline(always)] pub fn repeat_char( &self, f: &mut fmt::Formatter<'_>, c: char, count: usize, ) -> fmt::Result { if count > 0 { let s = std::iter::repeat(c).take(count).collect::(); write!(f, "{}", self.apply_to(s))?; } Ok(()) } /// Write a string several times with the line compound style /// /// Implementation Note: performances here are critical #[inline(always)] pub fn repeat_string(&self, f: &mut fmt::Formatter<'_>, s: &str, count: usize) -> fmt::Result { if count > 0 { write!(f, "{}", self.apply_to(s.repeat(count))) } else { Ok(()) } } /// Write 0 or more spaces with the line's compound style #[inline(always)] pub fn repeat_space(&self, f: &mut fmt::Formatter<'_>, count: usize) -> fmt::Result { self.repeat_string(f, " ", count) } /// write the value with this style on the given /// writer pub fn queue(&self, w: &mut W, val: D) -> Result<()> where D: Clone + Display, W: std::io::Write, { w.queue(PrintStyledContent(self.apply_to(val)))?; Ok(()) } /// write the string with this style on the given /// writer pub fn queue_str>(&self, w: &mut W, s: S) -> Result<()> where W: std::io::Write, { self.queue(w, s.into()) } pub fn queue_fg(&self, w: &mut W) -> Result<()> where W: std::io::Write, { if let Some(fg) = self.object_style.foreground_color { w.queue(SetForegroundColor(fg))?; } Ok(()) } pub fn queue_bg(&self, w: &mut W) -> Result<()> where W: std::io::Write, { if let Some(bg) = self.object_style.background_color { w.queue(SetBackgroundColor(bg))?; } Ok(()) } /// Clear with the compound_style's background. /// /// ``` /// # use termimad::*; /// # use termimad::crossterm::terminal::ClearType; /// # let skin = MadSkin::default(); /// let mut w = std::io::stderr(); /// skin.paragraph.compound_style.clear(&mut w, ClearType::UntilNewLine).unwrap(); /// ``` pub fn clear(&self, w: &mut W, clear_type: ClearType) -> Result<()> where W: std::io::Write, { self.queue_bg(w)?; w.queue(Clear(clear_type))?; Ok(()) } pub fn style_char(&self, nude_char: char) -> StyledChar { StyledChar::new(self.clone(), nude_char) } pub fn attrs(&self) -> Attributes { self.object_style.attributes } } termimad-0.29.4/src/displayable_line.rs000064400000000000000000000013731046102023000161630ustar 00000000000000use { crate::{ line::FmtLine, skin::MadSkin, }, std::fmt, }; /// A facility to write just a line of a text. /// /// It's normally not necessary to use this: it's more an internal /// tool utility. pub struct DisplayableLine<'s, 'l, 'p> { pub skin: &'s MadSkin, pub line: &'p FmtLine<'l>, pub width: Option, // available width } impl<'s, 'l, 'p> DisplayableLine<'s, 'l, 'p> { pub const fn new(skin: &'s MadSkin, line: &'p FmtLine<'l>, width: Option) -> Self { DisplayableLine { skin, line, width } } } impl fmt::Display for DisplayableLine<'_, '_, '_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.skin.write_fmt_line(f, self.line, self.width, true) } } termimad-0.29.4/src/errors.rs000064400000000000000000000005321046102023000141730ustar 00000000000000use crate::{ fit::InsufficientWidthError, }; /// Termimad error type #[derive(thiserror::Error, Debug)] pub enum Error { #[error("IO error: {0}")] IO(#[from] std::io::Error), #[error("InsufficientWidth: {0}")] InsufficientWidth(#[from] InsufficientWidthError), } pub(crate) type Result = std::result::Result; termimad-0.29.4/src/events/escape_sequence.rs000064400000000000000000000015611046102023000173160ustar 00000000000000use { crate::crossterm::{ event::{ KeyCode, KeyEvent, KeyModifiers, }, }, std::fmt, }; /// An escape sequence made of key events /// /// Termimad's event source currently offers no /// guarantee *all* escape sequences are really /// catched. If you find a problem please contact /// me. /// Note also that some escape sequence never /// come this high, being catched by crossterm /// before. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct EscapeSequence { pub keys: Vec, } impl fmt::Display for EscapeSequence { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for key in &self.keys { if let KeyEvent { code: KeyCode::Char(c), modifiers: KeyModifiers::NONE, .. } = key { write!(f, "{}", c)?; } } Ok(()) } } termimad-0.29.4/src/events/event_source.rs000064400000000000000000000261261046102023000166730ustar 00000000000000use { super::{ TimedEvent, EscapeSequence, }, crate::{ errors::Error, crossterm::{ self, event::{ Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, }, terminal, }, }, crokey::Combiner, crossbeam::channel::{bounded, unbounded, Receiver, Sender}, std::{ sync::{ atomic::{AtomicUsize, Ordering}, Arc, }, thread, time::{Duration, Instant}, } }; const DOUBLE_CLICK_MAX_DURATION: Duration = Duration::from_millis(700); const ESCAPE_SEQUENCE_CHANNEL_SIZE: usize = 10; struct TimedClick { time: Instant, x: u16, y: u16, } pub struct EventSourceOptions { /// Whether to try combine key events into key combinations. /// This changes the behavior of the terminal, if it's compatible, then restores /// the standard behavior on drop. pub combine_keys: bool, /// When combining is enabled, you may either want "simple" keys /// (i.e. without modifier or space) to be handled on key press, /// or to wait for a key release so that maybe they may /// be part of a combination like 'a-b'. /// If combinations without modifier or space are unlikely in your /// application, you may make it feel snappier by setting this to true. /// /// This setting has no effect when combining isn't enabled. pub mandate_modifier_for_multiple_keys: bool, /// Whether to filter out raw key events (default true) /// (if you want to manage repeat, press, release, specifically, you're probably /// not interested in combining keys) pub discard_raw_key_events: bool, /// whether to filter out simple mouse moves (default true) pub discard_mouse_move: bool, /// whether to filter out mouse drag (default false) pub discard_mouse_drag: bool, } /// a thread backed event listener emmiting events on a channel. /// /// The event source enables the terminal's raw mode and restores /// it on drop /// /// Additionnally to emmitting events, this source updates a /// sharable event count, protected by an Arc. This makes /// it easy for background computation to stop (or check if /// they should) when a user event is produced. /// /// The event source isn't tick based. It makes it possible to /// built TUI with no CPU consumption while idle. pub struct EventSource { is_combining_keys: bool, rx_events: Receiver, rx_seqs: Receiver, tx_quit: Sender, event_count: Arc, } impl Default for EventSourceOptions { fn default() -> Self { Self { combine_keys: false, mandate_modifier_for_multiple_keys: true, discard_raw_key_events: true, discard_mouse_move: true, discard_mouse_drag: false, } } } fn is_seq_start(key: KeyEvent) -> bool { key.code == KeyCode::Char('_') && key.modifiers == KeyModifiers::ALT } fn is_seq_end(key: KeyEvent) -> bool { key.code == KeyCode::Char('\\') && key.modifiers == KeyModifiers::ALT } impl EventSource { /// create a new source with default options /// /// If desired, mouse support must be enabled and disabled in crossterm. pub fn new() -> Result { Self::with_options(EventSourceOptions::default()) } /// return true if the source is configured to combine standard keys /// and the terminal supports it (it requires the 'kitty keyboard /// protocol'). /// /// If true, you may receive events with multiple non-modifier keys, /// eg `ctrl-a-b`. If not, the same sequence of keys will be received /// as two successive combinations: `ctrl-a` and `ctrl-b`. /// /// Combining is not delay-based: you receive the combination as soon /// as the keys are released (or as soon as the key is pressed in /// most cases when `mandate_modifier_for_multiple_keys` is true). pub fn supports_multi_key_combinations(&self) -> bool { self.is_combining_keys } /// create a new source /// /// If desired, mouse support must be enabled and disabled in crossterm. pub fn with_options(options: EventSourceOptions) -> Result { let mut combiner = Combiner::default(); terminal::enable_raw_mode()?; let is_combining_keys = if options.combine_keys { combiner.enable_combining()? } else { false }; combiner.set_mandate_modifier_for_multiple_keys(options.mandate_modifier_for_multiple_keys); let (tx_events, rx_events) = unbounded(); let (tx_seqs, rx_seqs) = bounded(ESCAPE_SEQUENCE_CHANNEL_SIZE); let (tx_quit, rx_quit) = unbounded(); let event_count = Arc::new(AtomicUsize::new(0)); let internal_event_count = Arc::clone(&event_count); thread::spawn(move || { let mut last_up: Option = None; let mut current_escape_sequence: Option = None; // return true when we must close the source let send_and_wait = |event| { internal_event_count.fetch_add(1, Ordering::SeqCst); if tx_events.send(event).is_err() { true // broken channel } else { match rx_quit.recv() { Ok(false) => false, _ => true, } } }; loop { let ct_event = match crossterm::event::read() { Ok(e) => e, _ => { continue; } }; let in_seq = current_escape_sequence.is_some(); if in_seq { if let crossterm::event::Event::Key(key) = ct_event { if is_seq_end(key) { // it's a proper sequence ending, we send it as such let mut seq = current_escape_sequence.take().unwrap(); seq.keys.push(key); if tx_seqs.try_send(seq).is_err() { // there's probably just nobody listening on // this zero size bounded channel } continue; } else if !key.modifiers.intersects(KeyModifiers::ALT | KeyModifiers::CONTROL) { // adding to the current escape sequence current_escape_sequence.as_mut().unwrap().keys.push(key); continue; } } // it's neither part of a proper sequence, nor the end // we send all previous events independently before sending this one let seq = current_escape_sequence.take().unwrap(); for key in seq.keys { let mut timed_event = TimedEvent::new(Event::Key(key)); timed_event.key_combination = combiner.transform(key); if options.discard_raw_key_events && timed_event.key_combination.is_none() { continue; } if send_and_wait(timed_event) { return; } } // the current event will be sent normally } else if let crossterm::event::Event::Key(key) = ct_event { if is_seq_start(key) { // starting a new sequence current_escape_sequence = Some(EscapeSequence { keys: vec![key] }); continue; } } if let Event::Mouse(mouse_event) = ct_event { if options.discard_mouse_move && mouse_event.kind == MouseEventKind::Moved { continue; } if options.discard_mouse_drag && matches!(mouse_event.kind, MouseEventKind::Drag(_)) { continue; } } let mut timed_event = TimedEvent::new(ct_event); if let Event::Key(key) = &timed_event.event { timed_event.key_combination = combiner.transform(*key); if options.discard_raw_key_events && timed_event.key_combination.is_none() { continue; } } if let Event::Mouse(MouseEvent { kind, column, row, .. }) = timed_event.event { if matches!( kind, MouseEventKind::Down(MouseButton::Left) | MouseEventKind::Up(MouseButton::Left) ) { if let Some(TimedClick { time, x, y }) = last_up { if column == x && row == y && timed_event.time - time < DOUBLE_CLICK_MAX_DURATION { timed_event.double_click = true; } } if kind == MouseEventKind::Up(MouseButton::Left) { last_up = Some(TimedClick { time: timed_event.time, x: column, y: row, }); } } } // we send the event to the receiver in the main event loop if send_and_wait(timed_event) { return; } } }); Ok(EventSource { is_combining_keys, rx_events, rx_seqs, tx_quit, event_count, }) } /// either start listening again, or quit, depending on the passed bool. /// It's mandatory to call this with quit=true at end for a proper ending /// of the thread (and its resources) pub fn unblock(&self, quit: bool) { self.tx_quit.send(quit).unwrap(); } /// return a shared reference to the event count. Other threads can /// use it to check whether something happened (when there's no /// parallel computation, the event channel is usually enough). pub fn shared_event_count(&self) -> Arc { Arc::clone(&self.event_count) } /// return a new receiver for the channel emmiting events pub fn receiver(&self) -> Receiver { self.rx_events.clone() } /// return a new receiver for the channel emmiting escape sequences /// /// It's a bounded channel and any escape sequence will be /// dropped when it's full pub fn escape_sequence_receiver(&self) -> Receiver { self.rx_seqs.clone() } } impl Drop for EventSource { fn drop(&mut self) { terminal::disable_raw_mode().unwrap(); } } termimad-0.29.4/src/events/mod.rs000064400000000000000000000002751046102023000147460ustar 00000000000000mod escape_sequence; mod event_source; mod timed_event; pub use { escape_sequence::EscapeSequence, event_source::{EventSource, EventSourceOptions}, timed_event::TimedEvent, }; termimad-0.29.4/src/events/timed_event.rs000064400000000000000000000050251046102023000164700ustar 00000000000000use { crate::crossterm::{ self, event::{ Event, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, }, }, crokey::{ KeyCombination, }, std::{ time::Instant, } }; /// A user event based on a crossterm event, decorated /// - with time /// - with a double_click flag /// - with a KeyCombination, if the event is a key ending /// a combination (which may be a simple key) /// /// You normally don't build this yourself, but rather use /// the [EventSource]. #[derive(Debug, Clone, PartialEq, Eq)] pub struct TimedEvent { pub time: Instant, pub event: crossterm::event::Event, /// false unless you set it yourself using the time /// or you get the timed event with an EventSource /// which computes it. Can be true only for left mouse /// down and left mouse up (both down and up of the second /// click have it true) pub double_click: bool, /// If you're interested in key combinations, you should /// prefer this field over the Key variant of the event /// field. If you want to react on Press or Repeat, then /// the event field holds the information. pub key_combination: Option, } impl TimedEvent { /// Wrap a crossterm event into a timed one, with time. /// /// You should normally not need to use this function, but rather obtain /// the timed event from an EventSource which build the normalized /// key combination, and sets the double_click flag. pub fn new(event: Event) -> Self { Self { time: Instant::now(), event, double_click: false, key_combination: None, } } /// If it's a simple mouse up and not determined to be the second click of /// a double click, return the coordinates pub fn as_click(&self) -> Option<(u16, u16)> { match self.event { Event::Mouse(MouseEvent { kind: MouseEventKind::Up(MouseButton::Left), column, row, modifiers: KeyModifiers::NONE, }) if !self.double_click => Some((column, row)), _ => None, } } pub fn is_key>(&self, key: K) -> bool { let key = key.into(); if self.key_combination == Some(key) { return true; } if let Event::Key(k) = &self.event { let self_key: KeyCombination = (*k).into(); if self_key == key { return true; } } false } } termimad-0.29.4/src/fit/composite_fit.rs000064400000000000000000000327441046102023000163170ustar 00000000000000use { crate::*, minimad::*, unicode_width::{ UnicodeWidthChar, UnicodeWidthStr, }, }; pub static ELLIPSIS: &str = "…"; /// A fitter can shorten a composite to make it fit a target width /// without wrapping (by removing parts and replacing them with /// ellipsis) #[derive(Debug, Clone, Copy)] pub struct Fitter { /// whether to try remove the central part of big "token" /// (parts without space nor style change) mid_token_ellision: bool, /// whether to try remove the central part of big compounds mid_compound_ellision: bool, align: Alignment, } impl Default for Fitter { fn default() -> Self { Self { mid_token_ellision: true, mid_compound_ellision: true, align: Alignment::Unspecified, } } } #[derive(Debug, Clone, Copy)] struct CharInfo { byte_idx: usize, width: usize, // virer } fn str_char_infos(s: &str) -> Vec { s.char_indices() .map(|(byte_idx, char)| CharInfo { byte_idx, width: char.width().unwrap_or(0), }) .collect() } #[derive(Debug, Clone)] struct Zone { compound_idx: usize, byte_start_idx: usize, char_infos: Vec, removable_width: usize, // cell width of string minus one character each end } impl Zone { fn token(compounds: &[Compound], min_removable_width: usize) -> Vec { let mut zones = Vec::new(); for (compound_idx, compound) in compounds.iter().enumerate() { let s = compound.src; if s.len() < min_removable_width + 2 { continue; } let mut byte_start: Option = None; for (byte_idx, char) in s.char_indices() { if char.is_whitespace() { if let Some(byte_start_idx) = byte_start { if byte_idx - byte_start_idx >= min_removable_width + 2 { let zs = &s[byte_start_idx..byte_idx]; let removable_width = zs.width(); if removable_width >= min_removable_width { let char_infos = str_char_infos(zs); zones.push(Zone { compound_idx, byte_start_idx, char_infos, removable_width, }); } } byte_start = None; } } else if byte_start.is_none() { byte_start = Some(byte_idx); } } if let Some(byte_start_idx) = byte_start { let byte_end_idx = s.len(); if byte_end_idx - byte_start_idx >= min_removable_width + 2 { let zs = &s[byte_start_idx..]; let removable_width = zs.width(); if removable_width >= min_removable_width { let char_infos = str_char_infos(zs); zones.push(Zone { compound_idx, byte_start_idx, char_infos, removable_width, }); } } } } zones } fn biggest_token(compounds: &[Compound], min_removable_width: usize) -> Option { Zone::token(compounds, min_removable_width) .drain(..) .max_by_key(|z| z.removable_width) } /// make a zone from each compound large enough fn compounds(compounds: &[Compound], min_removable_width: usize) -> Vec { compounds.iter() .enumerate() .filter_map(|(compound_idx, compound)| { let char_infos = str_char_infos(compound.src); if char_infos.len() < 2 + min_removable_width { return None; } let removable = &compound.src[char_infos[1].byte_idx..char_infos[char_infos.len()-1].byte_idx]; let removable_width = removable.width(); if removable_width < min_removable_width { None } else { Some(Zone { compound_idx, byte_start_idx: 0, char_infos, removable_width, }) } }) .collect() } fn biggest_compound(compounds: &[Compound], min_removable_width: usize) -> Option { Zone::compounds(compounds, min_removable_width) .drain(..) .max_by_key(|z| z.removable_width) } /// return the gain (that is the removed minus 1 for the ellipsis length) fn cut(&self, compounds: &mut Vec, to_remove: usize) -> usize { if self.removable_width < 2 { return 0; } let compound = &compounds[self.compound_idx]; let len = self.char_infos.len(); let mut start_char_idx = len / 2; let mut end_char_idx = start_char_idx; let mut removed_width = 0; loop { // we alternatively grow left and right if (end_char_idx-start_char_idx)%2 == 0 { if end_char_idx + 1 >= len { break; } end_char_idx += 1; } else { if start_char_idx <= 1 { break; } start_char_idx -= 1; } let start_byte_idx = self.byte_start_idx + self.char_infos[start_char_idx].byte_idx; let end_byte_idx = self.byte_start_idx + self.char_infos[end_char_idx].byte_idx; removed_width = (compound.src[start_byte_idx..end_byte_idx]).width(); if removed_width >= to_remove { break; } } let start_byte_idx = self.byte_start_idx + self.char_infos[start_char_idx].byte_idx; let end_byte_idx = self.byte_start_idx + self.char_infos[end_char_idx].byte_idx; let head = compound.sub(0, start_byte_idx); let tail = compound.tail(end_byte_idx); compounds[self.compound_idx] = head; compounds.insert(self.compound_idx+1, Compound::raw_str(ELLIPSIS)); compounds.insert(self.compound_idx+2, tail); removed_width - 1 } } impl Fitter { /// create a fitter for when you want a specific alignment. /// /// You may still change the mid_token_ellision and mid_compound_ellision /// later pub fn for_align(align: Alignment) -> Self { let internal_ellision = align == Alignment::Unspecified; Self { mid_token_ellision: internal_ellision, mid_compound_ellision: internal_ellision, align, } } /// ensure the composite fits the max_width, by replacing some parts /// with ellisions pub fn fit( self, fc: &mut FmtComposite<'_>, max_width: usize, skin: &MadSkin ) { // some special cases because they're hard to check after if fc.visible_length <= max_width { return; } else if max_width == 0 { fc.compounds.clear(); fc.visible_length = 0; return; } else if max_width == 1 { fc.compounds.clear(); fc.compounds.push(Compound::raw_str(ELLIPSIS)); fc.visible_length = 1; return; } let mut excess = fc.visible_length - max_width; // note: computing all zones once would be faster but would involve either // recomputing compound_idx ou finding another index scheme if self.mid_token_ellision { // cutting in the middle of big no space parts while excess > 0 { let mut gain = 0; if let Some(zone) = Zone::biggest_token(&fc.compounds, 3) { gain = zone.cut(&mut fc.compounds, excess + 1); } if gain == 0 { break; } excess -= gain.min(excess); } } if self.mid_compound_ellision { // cutting in the middle of big compounds while excess > 0 { let mut gain = 0; // we'll look for zones of removable width at least 2 // (because we put the ellipsis in place) if let Some(zone) = Zone::biggest_compound(&fc.compounds, 2) { gain = zone.cut(&mut fc.compounds, excess + 1); } if gain == 0 { break; } excess -= gain.min(excess); } } if excess == 0 { fc.recompute_width(skin); return; } let compounds = &mut fc.compounds; // we'll have to compensate with 1 or 2 ellipsis, so the "excess" is // increased accordingly we increase let (mut excess_left, mut excess_right) = match self.align { Alignment::Right => (excess + 1, 0), Alignment::Left | Alignment:: Unspecified => (0, excess + 1), Alignment::Center => { let left = excess / 2; let right = excess - left; if left > 0 { (left + 1, right + 1) } else { (0, right + 1) } }, }; if excess_left > 0 { // left truncating while excess_left > 0 && !compounds.is_empty() { let compound = &mut compounds[0]; let char_infos = str_char_infos(compound.src); let mut last_removed_char_idx = 0; let mut removed_width = 0; loop { removed_width += char_infos[last_removed_char_idx].width; if removed_width >= excess_left || last_removed_char_idx + 1 == char_infos.len() { break; } last_removed_char_idx += 1; } if last_removed_char_idx + 1 == char_infos.len() { // we remove the whole compound compounds.remove(0); excess_left -= removed_width.min(excess_left); } else { // we cut the left part compound.src = &compound.src[char_infos[last_removed_char_idx+1].byte_idx..]; excess_left = 0; } } compounds.insert(0, Compound::raw_str(ELLIPSIS)); } if excess_right > 0 { // right truncating while excess_right > 0 && !compounds.is_empty() { let last_idx = compounds.len()-1; let compound = &mut compounds[last_idx]; let char_infos = str_char_infos(compound.src); let mut removed_width = 0; let mut end_byte_idx = compound.src.len(); for ci in char_infos.iter().rev() { end_byte_idx = ci.byte_idx; removed_width += ci.width; if removed_width >= excess_right { break; } } if end_byte_idx == 0 { // we remove the whole compound compounds.pop(); excess_right -= removed_width.min(excess_right); } else { // we cut the right part compound.src = &compound.src[..end_byte_idx]; excess_right = 0; } } compounds.push(Compound::raw_str(ELLIPSIS)); } fc.recompute_width(skin); } } /// Tests of fitting, that is cutting the composite at best to make it /// fit a given width (if possible) /// /// The print which happens in case of failure isn't really well /// formatted. A solution if a test fails is to do /// cargo test fit_tests -- --nocapture #[cfg(test)] mod fit_tests { use minimad::{ Alignment, Composite, }; use crate::{ Fitter, FmtComposite, }; fn check_fit_align(src: &str, target_width: usize, align: Alignment) { dbg!((target_width, align)); let skin = crate::get_default_skin(); let mut fc = FmtComposite::from(Composite::from_inline(src), skin); let fitter = Fitter::for_align(align); fitter.fit(&mut fc, target_width, skin); dbg!(&fc); assert!(fc.visible_length <= target_width); // can be smaller } fn check_fit(src: &str, target_width: usize) { check_fit_align(src, target_width, Alignment::Right); check_fit_align(src, target_width, Alignment::Left); check_fit_align(src, target_width, Alignment::Center); check_fit_align(src, target_width, Alignment::Unspecified); } #[test] fn test_fit() { let sentence = "This sentence has **short** and **much longer** parts, and some Korean: *一曰道,二曰天*."; check_fit(sentence, 60); check_fit(sentence, 40); let five_issues = "一曰道,二曰天,三曰地,四曰將,五曰法。"; check_fit(five_issues, 15); check_fit(five_issues, 8); let status = "ab *cd* `12345 123456789`"; check_fit(status, 17); check_fit(status, 2); } } termimad-0.29.4/src/fit/crop_writer.rs000064400000000000000000000111111046102023000157730ustar 00000000000000use { crate::*, crate::crossterm::{style::Print, QueueableCommand}, std::borrow::Cow, unicode_width::UnicodeWidthChar, }; /// wrap a writer to ensure that at most `allowed` columns are /// written. pub struct CropWriter<'w, W> where W: std::io::Write, { pub w: &'w mut W, /// number of screen columns which may be covered pub allowed: usize, /// the string replacing a tabulation pub tab_replacement: &'static str, } impl<'w, W> CropWriter<'w, W> where W: std::io::Write, { pub fn new(w: &'w mut W, limit: usize) -> Self { Self { w, allowed: limit, tab_replacement: DEFAULT_TAB_REPLACEMENT, } } pub const fn is_full(&self) -> bool { self.allowed == 0 } /// return a tuple containing a string containing either the given &str /// or the part fitting the remaining width, and the width of this string) pub fn cropped_str<'a>(&self, s: &'a str) -> (Cow<'a, str>, usize) { StrFit::make_cow(s, self.allowed) } pub fn queue_unstyled_str(&mut self, s: &str) -> Result<(), Error> { if self.is_full() { return Ok(()); } let (string, len) = self.cropped_str(s); self.allowed -= len; self.w.queue(Print(string))?; Ok(()) } pub fn queue_str(&mut self, cs: &CompoundStyle, s: &str) -> Result<(), Error> { if self.is_full() { return Ok(()); } let (string, len) = self.cropped_str(s); self.allowed -= len; cs.queue(self.w, string) } pub fn queue_char(&mut self, cs: &CompoundStyle, c: char) -> Result<(), Error> { let width = UnicodeWidthChar::width(c).unwrap_or(0); if width < self.allowed { self.allowed -= width; cs.queue(self.w, c)?; } Ok(()) } pub fn queue_unstyled_char(&mut self, c: char) -> Result<(), Error> { if c == '\t' { return self.queue_unstyled_str(self.tab_replacement); } let width = UnicodeWidthChar::width(c).unwrap_or(0); if width < self.allowed { self.allowed -= width; self.w.queue(Print(c))?; } Ok(()) } /// a "g_string" is a "gentle" one: each char takes one column on screen. /// This function must thus not be used for unknown strings. pub fn queue_unstyled_g_string(&mut self, mut s: String) -> Result<(), Error> { if self.is_full() { return Ok(()); } let mut len = 0; for (idx, _) in s.char_indices() { len += 1; if len > self.allowed { s.truncate(idx); self.allowed = 0; self.w.queue(Print(s))?; return Ok(()); } } self.allowed -= len; self.w.queue(Print(s))?; Ok(()) } /// a "g_string" is a "gentle" one: each char takes one column on screen. /// This function must thus not be used for unknown strings. pub fn queue_g_string(&mut self, cs: &CompoundStyle, mut s: String) -> Result<(), Error> { if self.is_full() { return Ok(()); } let mut len = 0; for (idx, _) in s.char_indices() { len += 1; if len > self.allowed { s.truncate(idx); self.allowed = 0; return cs.queue(self.w, s); } } self.allowed -= len; cs.queue(self.w, s) } pub fn queue_fg(&mut self, cs: &CompoundStyle) -> Result<(), Error> { cs.queue_fg(self.w) } pub fn queue_bg(&mut self, cs: &CompoundStyle) -> Result<(), Error> { cs.queue_bg(self.w) } pub fn fill(&mut self, cs: &CompoundStyle, filling: &'static Filling) -> Result<(), Error> { self.repeat(cs, filling, self.allowed) } pub fn fill_unstyled(&mut self, filling: &'static Filling) -> Result<(), Error> { self.repeat_unstyled(filling, self.allowed) } pub fn fill_with_space(&mut self, cs: &CompoundStyle) -> Result<(), Error> { self.repeat(cs, &SPACE_FILLING, self.allowed) } pub fn repeat( &mut self, cs: &CompoundStyle, filling: &'static Filling, mut len: usize, ) -> Result<(), Error> { len = len.min(self.allowed); self.allowed -= len; filling.queue_styled(self.w, cs, len) } pub fn repeat_unstyled(&mut self, filling: &'static Filling, mut len: usize) -> Result<(), Error> { len = len.min(self.allowed); self.allowed -= len; filling.queue_unstyled(self.w, len) } } termimad-0.29.4/src/fit/filling.rs000064400000000000000000000025261046102023000150720ustar 00000000000000 use { crate::*, crate::crossterm::{ QueueableCommand, style::Print, }, std::io::Write, }; const FILLING_STRING_CHAR_LEN: usize = 1000; /// something to fill with pub struct Filling { filling_string: String, char_size: usize, } impl Filling { pub fn from_char(filling_char: char) -> Self { let char_size = String::from(filling_char).len(); let mut filling_string = String::with_capacity(char_size * FILLING_STRING_CHAR_LEN); for _ in 0..FILLING_STRING_CHAR_LEN { filling_string.push(filling_char); } Self { filling_string, char_size, } } pub fn queue_unstyled( &self, w: &mut W, mut len: usize, ) -> Result<(), Error> { while len > 0 { let sl = len.min(FILLING_STRING_CHAR_LEN); w.queue(Print(&self.filling_string[0..sl * self.char_size]))?; len -= sl; } Ok(()) } pub fn queue_styled( &self, w: &mut W, cs: &CompoundStyle, mut len: usize, ) -> Result<(), Error> { while len > 0 { let sl = len.min(FILLING_STRING_CHAR_LEN); cs.queue_str(w, &self.filling_string[0..sl * self.char_size])?; len -= sl; } Ok(()) } } termimad-0.29.4/src/fit/fit_error.rs000064400000000000000000000005561046102023000154420ustar 00000000000000use { std::fmt, }; /// Error thrown when fitting isn't possible #[derive(thiserror::Error, Debug)] pub struct InsufficientWidthError { pub available_width: usize, } impl fmt::Display for InsufficientWidthError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Insufficient available width ({})", self.available_width) } } termimad-0.29.4/src/fit/mod.rs000064400000000000000000000015011046102023000142150ustar 00000000000000//! This module contains various utilities related to //! writing in areas of limited sizes mod composite_fit; mod crop_writer; mod filling; mod fit_error; mod str_fit; mod tbl_fit; pub mod wrap; pub use { crate::Error, composite_fit::*, crop_writer::*, filling::*, fit_error::*, str_fit::*, tbl_fit::*, }; use { crate::crossterm::{ style::{Color, SetBackgroundColor}, QueueableCommand, }, minimad::once_cell::sync::Lazy, }; pub static DEFAULT_TAB_REPLACEMENT: &str = " "; pub static SPACE_FILLING: Lazy = Lazy::new(|| { Filling::from_char(' ') }); pub fn fill_bg( w: &mut W, len: usize, bg: Color, ) -> Result<(), Error> where W: std::io::Write, { w.queue(SetBackgroundColor(bg))?; SPACE_FILLING.queue_unstyled(w, len)?; Ok(()) } termimad-0.29.4/src/fit/str_fit.rs000064400000000000000000000103741046102023000151200ustar 00000000000000use { unicode_width::UnicodeWidthChar, std::borrow::Cow, }; pub static TAB_REPLACEMENT: &str = " "; /// Information about the fitting of a string into a given /// width in cols. /// /// The implementation here properly takes into account /// the width of special characters. /// /// Backspaces are considered as having a width of -1. /// /// This implementation is based on a replacement of the /// tab character. #[derive(Debug, Clone, Copy)] pub struct StrFit { bytes_count: usize, cols_count: usize, has_tab: bool, } impl StrFit { pub fn from(s: &str, cols_max: usize) -> Self { let mut bytes_count = 0; let mut cols_count: i32 = 0; let mut has_tab = false; for (idx, c) in s.char_indices() { let char_width: i32 = match c { '\t' => { // tab has_tab = true; TAB_REPLACEMENT.len() as i32 } '\x08' => { // backspace -1 } _ => UnicodeWidthChar::width(c).map(|w| w as i32).unwrap_or(0), }; let next_str_width = cols_count + char_width; if next_str_width > 0 && next_str_width as usize > cols_max { break; } cols_count = next_str_width; bytes_count = idx + c.len_utf8(); } Self { bytes_count, cols_count: cols_count.max(0) as usize, has_tab, } } /// return the counts in bytes and columns of the longest substring /// fitting the given number of columns pub fn count_fitting(s: &str, cols_max: usize) -> (usize, usize) { let fit = StrFit::from(s, cols_max); (fit.bytes_count, fit.cols_count) } /// return both the longest fitting string and the number of cols /// it takes on screen. /// /// We don't build a string around the whole str, which could be costly /// if it's very big pub fn make_string(s: &str, cols_max: usize) -> (String, usize) { let fit = StrFit::from(s, cols_max); if fit.has_tab { let string = (s[0..fit.bytes_count]).replace('\t', TAB_REPLACEMENT); (string, fit.cols_count) } else { (s[0..fit.bytes_count].to_string(), fit.cols_count) } } /// return both the longest fitting string and the number of cols /// it takes on screen. /// /// We don't build a string around the whole str, which could be costly /// if it's very big /// In case there's no tab in the input string, we can return a pointer over /// part of the original str) pub fn make_cow(s: &str, cols_max: usize) -> (Cow, usize) { let fit = StrFit::from(s, cols_max); if fit.has_tab { // we can't just borrow, as we insert chars let string = (s[0..fit.bytes_count]).replace('\t', TAB_REPLACEMENT); (Cow::Owned(string), fit.cols_count) } else { (Cow::Borrowed(&s[0..fit.bytes_count]), fit.cols_count) } } } #[cfg(test)] mod fitting_count_tests { use super::*; #[test] fn test_count_fitting() { assert_eq!(StrFit::count_fitting("test", 3), (3, 3)); assert_eq!(StrFit::count_fitting("test", 5), (4, 4)); let c12 = "Comunicações"; // normalized string (12 characters, 14 bytes) assert_eq!(c12.len(), 14); assert_eq!(c12.chars().count(), 12); assert_eq!(StrFit::count_fitting(c12, 12), (14, 12)); assert_eq!(StrFit::count_fitting(c12, 10), (12, 10)); assert_eq!(StrFit::count_fitting(c12, 11), (13, 11)); let c14 = "Comunicações"; // unnormalized string (14 characters, 16 bytes) assert_eq!(c14.len(), 16); assert_eq!(c14.chars().count(), 14); assert_eq!(StrFit::count_fitting(c14, 12), (16, 12)); let ja = "概要"; // each char takes 3 bytes and 2 columns assert_eq!(ja.len(), 6); assert_eq!(ja.chars().count(), 2); assert_eq!(StrFit::count_fitting(ja, 1), (0, 0)); assert_eq!(StrFit::count_fitting(ja, 2), (3, 2)); assert_eq!(StrFit::count_fitting(ja, 3), (3, 2)); assert_eq!(StrFit::count_fitting(ja, 4), (6, 4)); assert_eq!(StrFit::count_fitting(ja, 5), (6, 4)); } } termimad-0.29.4/src/fit/tbl_fit.rs000064400000000000000000000131611046102023000150660ustar 00000000000000use { crate::InsufficientWidthError, std::{ cmp, }, }; /// A fitter, accumulating data about the table which must fit into /// a given width, then computing the best column widths. pub struct TblFit { cols: Vec, available_sum_width: usize, } /// Information observed with calls to see_cell #[derive(Debug, Clone, Copy)] struct ColData { sum_widths: usize, width: usize, count: usize, } impl Default for ColData { fn default() -> Self { Self { sum_widths: 0, width: 3, // minimal col width count: 0, // number of observations on which we summed } } } impl ColData { const fn avg_width(self) -> usize { if self.count == 0 { 0 } else { div_ceil(self.sum_widths, self.count) } } pub fn see_cell(&mut self, cell_width: usize) { self.sum_widths += cell_width; self.width = self.width.max(cell_width); self.count += 1; } } /// Result of the fitting operation (always a success) pub struct TblFitResult { /// whether some cells will have to be wrapped pub reduced: bool, /// the widths of all columns, so that they're guaranteed to fit /// into the available_width (taking the borders into account) pub col_widths: Vec, } impl TblFit { /// Build a new fitter, or return an error if the width isn't enough /// for the given number of columns. /// /// available_width: total available width, including external borders pub fn new(cols_count: usize, available_width: usize) -> Result { if available_width < cols_count*4 + 1 { return Err(InsufficientWidthError { available_width }); } let cols = vec![ColData::default(); cols_count]; let available_sum_width = available_width - 1 - cols_count; Ok(Self { cols, available_sum_width, }) } pub fn see_cell(&mut self, col_idx: usize, cell_width: usize) { if let Some(col) = self.cols.get_mut(col_idx) { col.see_cell(cell_width); } } /// compute the fitting pub fn fit(&self) -> TblFitResult { let sum_widths: usize = self.cols.iter().map(|c| c.width).sum(); if sum_widths <= self.available_sum_width { return TblFitResult { reduced: false, col_widths: self.cols.iter().map(|c| c.width).collect(), }; } if self.cols.is_empty() { return TblFitResult { reduced: false, col_widths: Vec::new(), }; } if self.cols.len() == 1 { return TblFitResult { reduced: false, col_widths: vec![self.available_sum_width], }; } #[derive(Debug)] struct ColFit { idx: usize, // index of the col std_width: usize, // col internal max width avg_width: usize, // col internal average width width: usize, // final width } let mut fits: Vec = self.cols.iter() .enumerate() .map(|(idx, c)| ColFit { idx, std_width: c.width, avg_width: c.avg_width(), width: c.width, }) .collect(); if self.available_sum_width >= sum_widths { return TblFitResult { reduced: false, col_widths: self.cols.iter().map(|c| c.width).collect(), }; } let mut excess = sum_widths - self.available_sum_width; fits.sort_by_key(|c| cmp::Reverse(c.width)); // Step 1 // We do a first reduction, if possible, on columns wider // than 5, and trying to keep above the average width let potential_uncut_gain_1 = fits.iter() .filter(|c| c.width > 4 && c.width > c.avg_width + 1) .map(|c| (c.width - c.avg_width).min(4)) .sum::(); let potential_cut_gain_1 = potential_uncut_gain_1 .min(excess); if potential_cut_gain_1 > 0 { for c in fits.iter_mut() { if c.std_width > 4 && c.std_width > c.avg_width { let gain_1 = div_ceil((c.width - c.avg_width) * potential_cut_gain_1, potential_uncut_gain_1); let gain_1 = gain_1.min(excess).min(c.width - 4); c.width -= gain_1; excess -= gain_1; if excess == 0 { break; } } } } // Step 2 // We remove excess proportionnally if excess > 0 { let potential_total_gain_2 = fits.iter() .map(|c| c.width - 3) .sum::() .min(excess); let excess_before_2 = excess; for c in fits.iter_mut() { let gain_2 = div_ceil((c.width - 3) * excess_before_2, potential_total_gain_2); let gain_2 = gain_2.min(excess).min(c.width - 3); c.width -= gain_2; excess -= gain_2; if excess == 0 { break; } } } // it should be OK now fits.sort_by_key(|c| c.idx); TblFitResult { reduced: true, col_widths: fits.iter().map(|c| c.width).collect(), } } } /// divide, rounding to the top const fn div_ceil(q: usize, r: usize) -> usize { let mut res = q / r; if q%r != 0 { res += 1; } res } termimad-0.29.4/src/fit/wrap.rs000064400000000000000000000244421046102023000144200ustar 00000000000000#[allow(unused_imports)] use { crate::{ *, minimad::*, }, unicode_width::UnicodeWidthStr, }; /// build a composite which can be a new line after wrapping. fn follow_up_composite<'s>( fc: &FmtComposite<'s>, skin: &MadSkin, ) -> FmtComposite<'s> { let kind = match fc.kind { CompositeKind::ListItem(l) => CompositeKind::ListItemFollowUp(l), k => k, }; let visible_length = match kind { CompositeKind::ListItemFollowUp(l) if skin.list_items_indentation_mode == ListItemsIndentationMode::Block => 2+l as usize, CompositeKind::Quote => 2, _ => 0, }; FmtComposite { kind, compounds: Vec::new(), visible_length, spacing: fc.spacing, } } /// return the inherent widths related to the kind, the one of the first line (for /// example with a bullet) and the ones for the next lines (for example with quotes) pub fn composite_kind_widths( composite_kind: CompositeKind, skin: &MadSkin, ) -> (usize, usize) { match composite_kind { CompositeKind::Paragraph => (0, 0), CompositeKind::Header(_) => (0, 0), CompositeKind::ListItem(depth) => { let indent = 2 + depth as usize; match skin.list_items_indentation_mode { ListItemsIndentationMode::FirstLineOnly => (indent, 0), ListItemsIndentationMode::Block => (indent, indent), } } CompositeKind::ListItemFollowUp(depth) => { let indent = 2 + depth as usize; match skin.list_items_indentation_mode { ListItemsIndentationMode::FirstLineOnly => (0, 0), ListItemsIndentationMode::Block => (indent, indent), } } CompositeKind::Code => (0, 0), CompositeKind::Quote => (2, 2), } } /// cut the passed composite in several composites fitting the given *visible* width /// (which might be bigger or smaller than the length of the underlying string). /// width can't be less than 3. pub fn hard_wrap_composite<'s, 'c>( src_composite: &'c FmtComposite<'s>, width: usize, skin: &MadSkin, ) -> Result>, InsufficientWidthError> { if width < 3 { return Err(InsufficientWidthError{ available_width: width }); } debug_assert!(src_composite.visible_length > width); // or we shouldn't be called let mut composites: Vec> = Vec::new(); let (first_width, other_widths) = composite_kind_widths(src_composite.kind, skin); let mut dst_composite = FmtComposite { kind: src_composite.kind, compounds: Vec::new(), visible_length: first_width, spacing: src_composite.spacing, }; // Strategy 1: // we try to optimize for a quite frequent case: two parts with nothing or just space in // between let compounds = &src_composite.compounds; if ( // clean cut of 2 compounds.len() == 2 && compounds[0].src.width() + first_width <= width && compounds[1].src.width() + other_widths <= width ) || ( // clean cut of 3 compounds.len() == 3 && compounds[0].src.width() + first_width <= width && compounds[2].src.width() + other_widths <= width && compounds[1].src.chars().all(char::is_whitespace) ) { dst_composite.add_compound(compounds[0].clone()); let mut new_dst_composite = follow_up_composite(&dst_composite, skin); composites.push(dst_composite); new_dst_composite.add_compound(compounds[compounds.len()-1].clone()); composites.push(new_dst_composite); return Ok(composites); } let mut tokens = tokenize(&src_composite.compounds, width - first_width); // Strategy 2: // we try to cut along tokens, using spaces to break for token in tokens.drain(..) { // TODO: does that really take first_width into account ? if dst_composite.visible_length + token.width > width { if !token.blank { // we skip blank composite at line change let mut repl_composite = follow_up_composite(&dst_composite, skin); std::mem::swap(&mut dst_composite, &mut repl_composite); composites.push(repl_composite); dst_composite.add_compound(token.to_compound()); } } else { dst_composite.add_compound(token.to_compound()); } } composites.push(dst_composite); Ok(composites) } /// hard_wrap all normal lines to ensure the text fits the width. /// Doesn't touch table rows. /// Consumes the passed array and return a new one (may contain /// the original lines, avoiding cloning when possible). /// Return an error if the width is less than 3. pub fn hard_wrap_lines<'s>( src_lines: Vec>, width: usize, skin: &MadSkin, ) -> Result>, InsufficientWidthError> { let mut src_lines = src_lines; let mut lines = Vec::new(); for src_line in src_lines.drain(..) { if let FmtLine::Normal(fc) = src_line { let (left_margin, right_margin) = skin .line_style(fc.kind) .margins_in(Some(width)); if fc.visible_length + left_margin + right_margin <= width { lines.push(FmtLine::Normal(fc)); } else { for fc in hard_wrap_composite(&fc, width - left_margin - right_margin, skin)? { lines.push(FmtLine::Normal(fc)); } } } else { lines.push(src_line); } } Ok(lines) } /// Tests of hard wrapping /// /// The print which happens in case of failure isn't really well /// formatted. A solution if a test fails is to do /// cargo test -- --nocapture #[cfg(test)] mod wrap_tests { use { crate::{ displayable_line::DisplayableLine, skin::MadSkin, fit::wrap::*, }, }; fn visible_fmt_line_length(skin: &MadSkin, line: &FmtLine<'_>) -> usize { match line { FmtLine::Normal(fc) => skin.visible_composite_length(fc.kind, &fc.compounds), _ => 0, // FIXME implement } } /// check that after hard wrap, no line is longer /// that required /// check also that no line is empty (the source text /// is assumed to have no empty line) fn check_no_overflow(skin: &MadSkin, src: &str, width: usize) { let text = skin.text(src, Some(width)); println!("------- test wrapping with width: {}", width); for line in &text.lines { let len = visible_fmt_line_length(skin, line); println!( "len:{: >4} | {}", len, DisplayableLine { skin, line, width: None, } ); assert!(len <= width); assert!(len > 0); } } /// check line lenghts are what is expected #[allow(clippy::needless_range_loop)] fn check_line_lengths(skin: &MadSkin, src: &str, width: usize, lenghts: Vec) { println!("====\ninput text:\n{}", &src); let text = skin.text(src, Some(width)); assert_eq!(text.lines.len(), lenghts.len(), "same number of lines"); println!("====\nwrapped text:\n{}", &text); for i in 0..lenghts.len() { assert_eq!( visible_fmt_line_length(skin, &text.lines[i]), lenghts[i], "expected length for line {} when wrapping at {}", i, width ); } } /// check many wrappings of a 4 lines text with 2 list items and /// some style #[test] fn check_hard_wrapping_simple_text() { let skin = crate::get_default_skin(); // build a text and check it let src = "This is a *long* line which needs to be **broken**.\n\ And the text goes on with a list:\n\ * short item\n\ * a *somewhat longer item* (with a part in **bold**)"; for width in 3..50 { check_no_overflow(skin, src, width); } check_line_lengths(skin, src, 25, vec![25, 19, 25, 7, 12, 25, 23]); } #[test] fn check_space_removing() { let skin = crate::get_default_skin(); let src = FmtComposite::from(Composite::from_inline("syntax coloring"), skin); println!("input:\n{:?}", &src); let wrapped = hard_wrap_composite(&src, 8, skin).unwrap(); println!("wrapped: {:?}", &wrapped); assert_eq!(wrapped.len(), 2); } fn first_compound(line: FmtLine) -> Option { match line { FmtLine::Normal(mut fc) => fc.compounds.drain(..).next(), _ => None, } } #[test] /// check the case of a wrapping occuring after a space and at the start of a compound /// see https://github.com/Canop/termimad/issues/17 fn check_issue_17() { let skin = crate::get_default_skin(); let src = "*Now I'll describe this example with more words than necessary, in order to be sure to demonstrate scrolling (and **wrapping**, too, thanks to long sentences).*"; let text = skin.text(src, Some(120)); assert_eq!(text.lines.len(), 2); assert_eq!( first_compound(text.lines.into_iter().nth(1).unwrap()), Some(Compound::raw_str("wrapping").bold().italic()), ); } #[test] /// check that we're not wrapping outside of char boudaries fn check_issue_23() { let md: &str = "ZA\u{360}\u{321}\u{34a}\u{35d}LGΌ IS\u{36e}\u{302}\u{489}\u{32f}\u{348}\u{355}\u{339}\u{318}\u{331} TO\u{345}\u{347}\u{339}\u{33a}Ɲ\u{334}ȳ\u{333} TH\u{318}E\u{344}\u{309}\u{356} \u{360}P\u{32f}\u{34d}\u{32d}O\u{31a}\u{200b}N\u{310}Y\u{321} H\u{368}\u{34a}\u{33d}\u{305}\u{33e}\u{30e}\u{321}\u{338}\u{32a}\u{32f}E\u{33e}\u{35b}\u{36a}\u{344}\u{300}\u{301}\u{327}\u{358}\u{32c}\u{329} \u{367}\u{33e}\u{36c}\u{327}\u{336}\u{328}\u{331}\u{339}\u{32d}\u{32f}C\u{36d}\u{30f}\u{365}\u{36e}\u{35f}\u{337}\u{319}\u{332}\u{31d}\u{356}O\u{36e}\u{34f}\u{32e}\u{32a}\u{31d}\u{34d}"; let skin = MadSkin::default(); for w in 40..60 { println!("wrapping on width {}", w); let _text = FmtText::from(&skin, md, Some(w)); } } } termimad-0.29.4/src/inline.rs000064400000000000000000000010761046102023000141410ustar 00000000000000use std::fmt; use crate::composite::FmtComposite; use crate::skin::MadSkin; /// A directly printable markdown snippet, complete /// with the reference to a skin so that it can /// implement the Display trait. /// /// Use this when you don't have a text but just /// part of a line pub struct FmtInline<'k, 's> { pub skin: &'k MadSkin, pub composite: FmtComposite<'s>, } impl fmt::Display for FmtInline<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.skin.write_fmt_composite(f, &self.composite, None, false, true) } } termimad-0.29.4/src/lib.rs000064400000000000000000000154151046102023000134330ustar 00000000000000/*! This crate lets you display simple markdown snippets or scrollable wrapped markdown texts in the terminal. In order to use Termimad you typically need * some *markdown*: a string which you can have loaded or dynamically built * a *skin*: which defines the colors and style attributes of every parts Additionnaly, you might define an *area* of the screen in which to draw (and maybe scroll). # The skin It's an instance of [`MadSkin`](struct.MadSkin.html) whose fields you customize according to your tastes or (better) to your application's configuration. ```rust use termimad::crossterm::style::{Color::*, Attribute::*}; use termimad::*; // start with the default skin let mut skin = MadSkin::default(); // let's decide bold is in light gray skin.bold.set_fg(gray(20)); // let's make strikeout not striked out but red, with no specific background, and bold skin.strikeout = CompoundStyle::new(Some(Red), None, Bold.into()); ``` **Beware:** * you may define colors in full [`rgb`](fn.rgb.html) but this will limit compatibility with old terminals. It's recommended to stick to [Ansi colors](fn.ansi.html), [gray levels](fn.gray.html), or [Crossterm predefined values](https://docs.rs/crossterm/0.9.6/crossterm/enum.Color.html). * styles are composed. For example a word may very well be italic, bold and striked out. It might not be wise to have them differ only by their background color for example. # Display a simple inline snippet ``` # use termimad::*; // with the default skin, nothing simpler: termimad::print_inline("value: **52**"); ``` # Print a text A multi-line markdown string can be printed the same way than an *inline* snippet, but you usually want it to be wrapped according to the available terminal width. ```rust,no_run # use termimad::*; # let skin = MadSkin::default(); # let my_markdown = "#title\n* item 1\n* item 2"; eprintln!("{}", skin.term_text(my_markdown)); ``` [`MadSkin`](struct.MadSkin.html) contains other functions to prepare a text for no specific size or for one which isn't the terminal's width. It also offers several functions to print it either on `stdout` or on a given `Write`. # Display a text, maybe scroll it A terminal application often uses an *alternate* screen instead of just dumping its text to stdout, and you often want to display in a specific rect of that screen, with adequate wrapping and not writing outside that rect. You may also want to display a scrollbar if the text doesn't fit the area. A [`MadView`](struct.MadView.html) makes that simple: ``` # use termimad::*; # let markdown = String::from("#title\n* item 1\n* item 2"); # let skin = MadSkin::default(); let area = Area::new(0, 0, 10, 12); let mut view = MadView::from(markdown, area, skin); view.write().unwrap(); ``` If you don't want to give ownership of the skin, markdown and area, you may prefer to use a [`TextView`](struct.TextView.html). You may see how to write a text viewer responding to key inputs to scroll a markdown text in [the scrollable example](https://github.com/Canop/termimad/blob/master/examples/scrollable/main.rs). # Templates In order to separate the rendering format from the content, the `format!` macro is not always a good solution because you may not be sure the content is free of characters which may mess the markdown. A solution is to use one of the templating functions or macros. Example: ``` # #[macro_use] extern crate minimad; # use termimad::*; # let skin = MadSkin::default(); mad_print_inline!( &skin, "**$0 formula:** *$1*", // the markdown template, interpreted once "Disk", // fills $0 "2*π*r", // fills $1. Note that the stars don't mess the markdown ); ``` Main difference with using `print!(format!( ... ))`: * the markdown parsing and template building are done only once (using `once_cell` internally) * the given values aren't interpreted as markdown fragments and don't impact the style * arguments can be omited, repeated, given in any order * no support for fmt parameters or arguments other than `&str` *(in the current version)* You'll find more examples and advice in the *templates* example. # Examples The repository contains several other examples, which hopefully cover the whole API while being simple enough. It's recommended you start by trying them or at least glance at their code. */ mod area; mod ask; mod code; mod color; mod composite; mod composite_kind; mod compound_style; mod displayable_line; mod errors; mod events; mod fit; mod inline; mod line; mod line_style; mod list_indentation; mod macros; mod parse; mod rect; mod scrollbar_style; mod serde; mod skin; mod spacing; mod styled_char; mod table_border_chars; mod tbl; mod text; mod tokens; mod views; pub use { area::{compute_scrollbar, terminal_size, Area}, ask::*, color::*, composite::FmtComposite, compound_style::*, composite_kind::*, displayable_line::DisplayableLine, errors::Error, events::{TimedEvent, EventSource, EventSourceOptions}, fit::*, inline::FmtInline, line::FmtLine, line_style::LineStyle, list_indentation::*, minimad::Alignment, parse::*, rect::*, scrollbar_style::ScrollBarStyle, skin::MadSkin, spacing::Spacing, styled_char::StyledChar, table_border_chars::*, text::FmtText, views::{ InputField, ListView, ListViewCell, ListViewColumn, MadView, ProgressBar, TextView, }, }; pub use coolor; pub use crokey::crossterm; pub use minimad; use tokens::*; /// Return a reference to the global skin /// /// If you want a new default skin so that you can set /// colors or styles, get a separate instance /// with `Skin::default()` instead. pub fn get_default_skin() -> &'static MadSkin { use minimad::once_cell::sync::Lazy; static DEFAULT_SKIN: Lazy = Lazy::new(MadSkin::default); &DEFAULT_SKIN } /// Return a formatted line, which implements Display. /// /// This uses the default skin. /// Don't use if you expect your markdown to be several lines. pub fn inline(src: &str) -> FmtInline<'_, '_> { get_default_skin().inline(src) } /// Return an unwrapped formatted text, implementing Display. /// /// This uses the default skin and doesn't wrap the lines /// at all. Most often you'll prefer to use `term_text` /// which makes a text wrapped for the current terminal. pub fn text(src: &str) -> FmtText<'_, '_> { get_default_skin().text(src, None) } /// Return a terminal wrapped formatted text, implementing Display. /// /// This uses the default skin and the terminal's width pub fn term_text(src: &str) -> FmtText<'_, '_> { get_default_skin().term_text(src) } /// Write a string interpreted as markdown with the default skin. pub fn print_inline(src: &str) { get_default_skin().print_inline(src); } /// Write a text interpreted as markdown with the default skin. pub fn print_text(src: &str) { get_default_skin().print_text(src); } termimad-0.29.4/src/line.rs000064400000000000000000000032031046102023000136040ustar 00000000000000use minimad::{Line, TableRule}; use crate::composite::FmtComposite; use crate::skin::MadSkin; use crate::tbl::{FmtTableRow, FmtTableRule, RelativePosition}; /// A line in a text. This structure should normally not be /// used outside of the lib. #[derive(Debug)] pub enum FmtLine<'s> { Normal(FmtComposite<'s>), TableRow(FmtTableRow<'s>), TableRule(FmtTableRule), HorizontalRule, } impl<'s> FmtLine<'s> { /// Build a fmtline from a minimad line. /// Skin is passed because it might affect the visible size /// in the future pub fn from(mline: Line<'s>, skin: &MadSkin) -> Self { match mline { Line::Normal(composite) => FmtLine::Normal(FmtComposite::from(composite, skin)), Line::TableRow(table_row) => FmtLine::TableRow(FmtTableRow::from(table_row, skin)), Line::TableRule(TableRule { cells }) => FmtLine::TableRule(FmtTableRule { position: RelativePosition::Other, widths: Vec::new(), aligns: cells, }), Line::HorizontalRule => FmtLine::HorizontalRule, Line::CodeFence(..) => FmtLine::HorizontalRule, // we're not supposed to get code fence in clean texts } } pub fn visible_length(&self) -> usize { match self { FmtLine::Normal(composite) => composite.visible_length, FmtLine::TableRow(row) => row.cells.iter().fold(0, |s, c| s + c.visible_length), // Is that right ? no spacing ? FmtLine::TableRule(rule) => 1 + rule.widths.iter().fold(0, |s, w| s + w + 1), FmtLine::HorizontalRule => 0, // No intrinsic width } } } termimad-0.29.4/src/line_style.rs000064400000000000000000000056221046102023000150330ustar 00000000000000use { crate::{ compound_style::CompoundStyle, crossterm::style::{Attribute, Color}, }, minimad::Alignment, std::fmt, }; /// A style applicable to a type of line. /// /// It's made of /// - the base style of the compounds /// - the alignment #[derive(Default, Clone, Debug, PartialEq)] pub struct LineStyle { pub compound_style: CompoundStyle, pub align: Alignment, pub left_margin: usize, pub right_margin: usize, } impl LineStyle { /// Return a (left_margin, right_margin) tupple, with both values /// being zeroed when they wouldn't let a width of at least 3 otherwise. pub fn margins_in(&self, available_width: Option) -> (usize, usize) { if let Some(width) = available_width { if width < self.left_margin + self.right_margin + 3 { return (0, 0); } } (self.left_margin, self.right_margin) } pub fn new( compound_style: CompoundStyle, align: Alignment, ) -> Self { Self { compound_style, align, left_margin: 0, right_margin: 0, } } /// Set the foreground color to the passed color. #[inline(always)] pub fn set_fg(&mut self, color: Color) { self.compound_style.set_fg(color); } /// Set the background color to the passed color. #[inline(always)] pub fn set_bg(&mut self, color: Color) { self.compound_style.set_bg(color); } /// Set the colors to the passed ones pub fn set_fgbg(&mut self, fg: Color, bg: Color) { self.compound_style.set_fgbg(fg, bg); } /// Add an `Attribute`. Like italic, underlined or bold. #[inline(always)] pub fn add_attr(&mut self, attr: Attribute) { self.compound_style.add_attr(attr); } /// Write a string several times with the line compound style #[inline(always)] pub fn repeat_string(&self, f: &mut fmt::Formatter<'_>, s: &str, count: usize) -> fmt::Result { self.compound_style.repeat_string(f, s, count) } /// Write a string several times with the line compound style #[inline(always)] pub fn repeat_char(&self, f: &mut fmt::Formatter<'_>, c: char, count: usize) -> fmt::Result { self.compound_style.repeat_char(f, c, count) } /// Write 0 or more spaces with the line's compound style #[inline(always)] pub fn repeat_space(&self, f: &mut fmt::Formatter<'_>, count: usize) -> fmt::Result { self.repeat_char(f, ' ', count) } pub fn blend_with>(&mut self, color: C, weight: f32) { self.compound_style.blend_with(color, weight); } } impl From for LineStyle { fn from(compound_style: CompoundStyle) -> Self { Self { compound_style, align: Alignment::Unspecified, left_margin: 0, right_margin: 0, } } } termimad-0.29.4/src/list_indentation.rs000064400000000000000000000006001046102023000162220ustar 00000000000000 /// Describe how list items are indented when the item has to be /// wrapped: either only the first line (the one with the bullet) /// or the whole item as a block. /// /// FirstLineOnly is more compact, but Block is prettier and more /// readable. #[derive(Clone, Copy, Debug, PartialEq, Default)] pub enum ListItemsIndentationMode { FirstLineOnly, #[default] Block, } termimad-0.29.4/src/macros.rs000064400000000000000000000043011046102023000141410ustar 00000000000000 /// print a markdown template, with other arguments taking `$0` to `$9` places in the template. /// /// Example: /// /// ``` /// use termimad::*; /// /// let skin = MadSkin::default(); /// mad_print_inline!( /// &skin, /// "**$0 formula:** *$1*", // the markdown template, interpreted once /// "Disk", // fills $0 /// "2*π*r", // fills $1. Note that the stars don't mess the markdown /// ); /// ``` #[macro_export] macro_rules! mad_print_inline { ($skin: expr, $md: literal $(, $value: expr )* $(,)? ) => {{ let vals: Vec = vec![$($value.to_string(),)*]; #[allow(unused_variables)] #[allow(unused_mut)] let mut i: usize = 0; use $crate::minimad::{once_cell::sync::Lazy, InlineTemplate}; static TEMPLATE: Lazy> = Lazy::new(|| { InlineTemplate::from($md) }); let mut composite = TEMPLATE.raw_composite(); for (arg_idx, val) in vals.iter().enumerate() { TEMPLATE.apply(&mut composite, arg_idx, val); } $skin.print_composite(composite) }}; } /// write a markdown template, with other arguments taking `$0` to `$9` places in the template. /// /// Example: /// /// ``` /// use termimad::*; /// /// let skin = MadSkin::default(); /// mad_write_inline!( /// &mut std::io::stdout(), /// &skin, /// "**$0 formula:** *$1*", // the markdown template, interpreted once /// "Disk", // fills $0 /// "2*π*r", // fills $1. Note that the stars don't mess the markdown /// ).unwrap(); /// ``` #[macro_export] macro_rules! mad_write_inline { ($w: expr, $skin: expr, $md: literal $(, $value: expr )* $(,)? ) => {{ use std::io::Write; let vals: Vec = vec![$($value.to_string(),)*]; let mut i: usize = 0; use $crate::minimad::{once_cell::sync::Lazy, InlineTemplate}; static TEMPLATE: Lazy> = Lazy::new(|| { InlineTemplate::from($md) }); let mut composite = TEMPLATE.raw_composite(); for (arg_idx, val) in vals.iter().enumerate() { TEMPLATE.apply(&mut composite, arg_idx, val); } $skin.write_composite($w, composite) }}; } termimad-0.29.4/src/parse/mod.rs000064400000000000000000000123421046102023000145520ustar 00000000000000//! The parse module provides a set of functions helping //! parse colors, compound styles, line styles, from strings mod parse_align; mod parse_attribute; mod parse_color; mod parse_compound_style; mod parse_line_style; mod parse_styled_char; pub use { parse_align::*, parse_attribute::*, parse_color::*, parse_compound_style::*, parse_line_style::*, parse_styled_char::*, }; use { crate::crossterm::style::{ Attribute, Color, }, lazy_regex::*, minimad::Alignment, std::{ fmt::{ self, Write, }, io, }, }; #[derive(thiserror::Error, Debug)] pub enum ParseStyleTokenError { #[error("{0} not recognized as a style token")] Unrecognized(String), #[error("Invalid color: {0}")] InvalidColor(#[from] ParseColorError), } /// something which may be part of a style #[derive(Debug, Clone, Copy, PartialEq)] pub enum StyleToken { Char(char), Color(Color), Attribute(Attribute), Align(Alignment), Dimension(u16), /// A specified absence, meaning for example "no foreground" None, } impl fmt::Display for StyleToken { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Char(c) => write!(f, "{}", c), Self::Color(c) => write_color(f, *c), Self::Attribute(a) => write_attribute(f, *a), Self::Align(a) => write_align(f, *a), Self::Dimension(number) => write!(f, "{}", number), Self::None => write!(f, "none"), } } } pub trait PushStyleTokens { fn push_style_tokens(&self, tokens: &mut Vec); fn to_style_tokens_string(&self) -> String { let mut tokens = Vec::new(); self.push_style_tokens(&mut tokens); let mut s = String::new(); for token in tokens { // safety: write! on a string can't fail if !s.is_empty() { write!(&mut s, " {token}").unwrap(); } else { write!(&mut s, "{token}").unwrap(); } } s } } pub fn write_style_tokens(w: &mut W, tokens: &[StyleToken]) -> io::Result<()> { let mut first = true; for token in tokens { if first { write!(w, " {token}")?; first = false; } else { write!(w, "{token}")?; } } Ok(()) } pub fn style_tokens_to_string(tokens: &[StyleToken]) -> String { let mut s = String::new(); for token in tokens { // safety: write! on a string can't fail if !s.is_empty() { write!(&mut s, " {token}").unwrap(); } else { write!(&mut s, "{token}").unwrap(); } } s } pub fn parse_style_token(s: &str) -> Result { if regex_is_match!("none"i, s) { return Ok(StyleToken::None); } if let Ok(number) = s.parse() { return Ok(StyleToken::Dimension(number)); } match parse_color(s) { Ok(color) => { return Ok(StyleToken::Color(color)); } Err(ParseColorError::Unrecognized) => {} Err(e) => { return Err(e.into()); } } if let Ok(attribute) = parse_attribute(s) { return Ok(StyleToken::Attribute(attribute)); } if let Ok(align) = parse_align(s) { return Ok(StyleToken::Align(align)); } let mut chars = s.chars(); let c = chars.next(); if let Some(c) = c { if chars.next().is_none() { return Ok(StyleToken::Char(c)); } } Err(ParseStyleTokenError::Unrecognized(s.to_owned())) } pub fn parse_style_tokens(s: &str) -> Result, ParseStyleTokenError> { let mut tokens = Vec::new(); for m in regex!(r#"[^\s()]+(\([\w,\s]+\))?"#).find_iter(s) { tokens.push(parse_style_token(m.as_str())?); } Ok(tokens) } #[test] fn test_parse_style_tokens() { use { crate::{ crossterm::style::Attribute::*, gray, rgb, }, minimad::Alignment::*, ParseStyleTokenError as E, StyleToken as T, }; assert_eq!( parse_style_tokens("red bold left").unwrap(), vec![T::Color(Color::Red), T::Attribute(Bold), T::Align(Left)], ); assert!(parse_style_tokens("red pissenlit").is_err()); assert_eq!( parse_style_tokens("Center grey(15) RGB(51, 47, 58) bold").unwrap(), vec![ T::Align(Center), T::Color(gray(15)), T::Color(rgb(51, 47, 58)), T::Attribute(Bold) ], ); assert_eq!( parse_style_tokens(" Yellow Italic ").unwrap(), vec![T::Color(Color::Yellow), T::Attribute(Italic)], ); assert_eq!( parse_style_tokens("| Yellow red").unwrap(), vec![T::Char('|'), T::Color(Color::Yellow), T::Color(Color::Red)], ); assert_eq!( parse_style_tokens("rgb(255,0,100) #fb0").unwrap(), vec![T::Color(rgb(255, 0, 100)), T::Color(rgb(255, 187, 0))], ); let parsed = parse_style_tokens(" red gray(40) "); if let Err(E::InvalidColor(ParseColorError::InvalidGreyLevel { level })) = parsed { assert_eq!(level, 40); } else { panic!("failed to fail"); }; } termimad-0.29.4/src/parse/parse_align.rs000064400000000000000000000015421046102023000162570ustar 00000000000000use { lazy_regex::*, minimad::Alignment, std::fmt, }; #[derive(thiserror::Error, Debug)] pub enum ParseAlignError { #[error("not a valid alignment")] Unrecognized, } pub fn write_align(f: &mut fmt::Formatter<'_>, a: Alignment) -> fmt::Result { match a { Alignment::Left => write!(f, "left"), Alignment::Center => write!(f, "center"), Alignment::Right => write!(f, "right"), Alignment::Unspecified => Ok(()), } } /// Read a Minimad Alignment from a string. pub fn parse_align(s: &str) -> Result { if regex_is_match!("left"i, s) { Ok(Alignment::Left) } else if regex_is_match!("center"i, s) { Ok(Alignment::Center) } else if regex_is_match!("right"i, s) { Ok(Alignment::Right) } else { Err(ParseAlignError::Unrecognized) } } termimad-0.29.4/src/parse/parse_attribute.rs000064400000000000000000000051471046102023000171750ustar 00000000000000use { crate::crossterm::style::Attribute, lazy_regex::*, std::fmt, }; #[derive(thiserror::Error, Debug)] pub enum ParseAttributeError { #[error("not a recognized attribute")] Unrecognized, } pub fn write_attribute(f: &mut fmt::Formatter<'_>, a: Attribute) -> fmt::Result { match a { Attribute::Reset => Ok(()), Attribute::Bold => write!(f, "Bold"), Attribute::Dim => write!(f, "Dim"), Attribute::Italic => write!(f, "Italic"), Attribute::Underlined => write!(f, "Underlined"), Attribute::SlowBlink => write!(f, "SlowBlink"), Attribute::RapidBlink => write!(f, "RapidBlink"), Attribute::Reverse => write!(f, "Reverse"), Attribute::Hidden => write!(f, "Hidden"), Attribute::CrossedOut => write!(f, "CrossedOut"), Attribute::Fraktur => write!(f, "Fraktur"), Attribute::NoBold => write!(f, "NoBold"), Attribute::NormalIntensity => write!(f, "NormalIntensity"), Attribute::NoItalic => write!(f, "NoItalic"), Attribute::NoUnderline => write!(f, "NoUnderline"), Attribute::NoBlink => write!(f, "NoBlink"), Attribute::NoReverse => write!(f, "NoReverse"), Attribute::NoHidden => write!(f, "NoHidden"), Attribute::NotCrossedOut => write!(f, "NotCrossedOut"), Attribute::Framed => write!(f, "Framed"), Attribute::Encircled => write!(f, "Encircled"), Attribute::OverLined => write!(f, "OverLined"), Attribute::NotFramedOrEncircled => write!(f, "NotFramedOrEncircled"), Attribute::NotOverLined => write!(f, "NotOverLined"), _ => Ok(()), } } /// Read a Minimad Attributement from a string. pub fn parse_attribute(s: &str) -> Result { if regex_is_match!("bold"i, s) { Ok(Attribute::Bold) } else if regex_is_match!("crossed[_-]?out"i, s) { Ok(Attribute::CrossedOut) } else if regex_is_match!("dim"i, s) { Ok(Attribute::Dim) } else if regex_is_match!("italic"i, s) { Ok(Attribute::Italic) } else if regex_is_match!("under[_-]?lined"i, s) { Ok(Attribute::Underlined) } else if regex_is_match!("over[_-]?lined"i, s) { Ok(Attribute::OverLined) } else if regex_is_match!("reverse"i, s) { Ok(Attribute::Reverse) } else if regex_is_match!("encircled"i, s) { Ok(Attribute::Encircled) } else if regex_is_match!("slow[_-]?blink"i, s) { Ok(Attribute::SlowBlink) } else if regex_is_match!("rapid[_-]?blink"i, s) { Ok(Attribute::RapidBlink) } else { Err(ParseAttributeError::Unrecognized) } } termimad-0.29.4/src/parse/parse_color.rs000064400000000000000000000105111046102023000162770ustar 00000000000000use { crate::{ ansi, crossterm::style::Color, gray, rgb, }, lazy_regex::*, std::fmt, }; #[derive(thiserror::Error, Debug)] pub enum ParseColorError { #[error("'not a recognized color")] Unrecognized, #[error("grey level must be between 0 and 23 (got {level})")] InvalidGreyLevel { level: u8 }, } pub fn write_color(f: &mut fmt::Formatter<'_>, c: Color) -> fmt::Result { match c { Color::Reset => Ok(()), Color::Black => write!(f, "Black"), Color::DarkGrey => write!(f, "DarkGrey"), Color::Red => write!(f, "Red"), Color::DarkRed => write!(f, "DarkRed"), Color::Green => write!(f, "Green"), Color::DarkGreen => write!(f, "DarkGreen"), Color::Yellow => write!(f, "Yellow"), Color::DarkYellow => write!(f, "DarkYellow"), Color::Blue => write!(f, "Blue"), Color::DarkBlue => write!(f, "DarkBlue"), Color::Magenta => write!(f, "Magenta"), Color::DarkMagenta => write!(f, "DarkMagenta"), Color::Cyan => write!(f, "Cyan"), Color::DarkCyan => write!(f, "DarkCyan"), Color::White => write!(f, "White"), Color::Grey => write!(f, "Grey"), Color::Rgb { r, g, b } => write!(f, "rgb({r}, {g}, {b})"), Color::AnsiValue(code) => write!(f, "ansi({code})"), } } /// Read a Crossterm color from a string. /// /// It may be either /// - one of the few known color name. Example: "darkred" /// - grayscale with level in [0,24[. Example: "grey(5)" /// - an Ansi code. Example "ansi(106)" /// - RGB. Example: "rgb(25, 100, 0)" pub fn parse_color(s: &str) -> Result { fn hex(s: &str) -> Result { if s.len() == 1 { u8::from_str_radix(&format!("{s}{s}"), 16) } else { u8::from_str_radix(s, 16) } } if let Some((_, value)) = regex_captures!(r"^ansi\((?P\d+)\)$"i, s) { let value = value.parse(); if let Ok(value) = value { return Ok(ansi(value)); // all ANSI values are ok } else { return Err(ParseColorError::Unrecognized); } } if let Some((_, level)) = regex_captures!(r"^gr[ae]y(?:scale)?\((?P\d+)\)$"i, s) { let level = level.parse(); if let Ok(level) = level { if level > 23 { return Err(ParseColorError::InvalidGreyLevel { level }); } return Ok(gray(level)); } else { return Err(ParseColorError::Unrecognized); } } if let Some((_, r, g, b)) = regex_captures!(r"^rgb\((?P\d+),\s*(?P\d+),\s*(?P\d+)\)$"i, s) { if let (Ok(r), Ok(g), Ok(b)) = (r.parse(), g.parse(), b.parse()) { return Ok(rgb(r, g, b)); } else { return Err(ParseColorError::Unrecognized); } } if let Some((_, r, g, b)) = regex_captures!(r"^#([\da-f]{1,2})([\da-f]{1,2})([\da-f]{1,2})$"i, s) { if let (Ok(r), Ok(g), Ok(b)) = (hex(r), hex(g), hex(b)) { return Ok(rgb(r, g, b)); } else { return Err(ParseColorError::Unrecognized); } } let s = s.to_lowercase(); match s.as_str() { "black" => Ok(Color::AnsiValue(16)), "blue" => Ok(Color::Blue), "cyan" => Ok(Color::Cyan), "darkblue" => Ok(Color::DarkBlue), "darkcyan" => Ok(Color::DarkCyan), "darkgreen" => Ok(Color::DarkGreen), "darkmagenta" => Ok(Color::DarkMagenta), "darkred" => Ok(Color::DarkRed), "green" => Ok(Color::Green), "grey" => Ok(Color::Grey), "magenta" => Ok(Color::Magenta), "red" => Ok(Color::Red), "yellow" => Ok(Color::Yellow), "darkyellow" => Ok(Color::DarkYellow), "white" => Ok(Color::AnsiValue(231)), _ => Err(ParseColorError::Unrecognized), } } #[test] fn test_parse_color() { assert_eq!(parse_color("rgb(255, 35, 45)").unwrap(), rgb(255, 35, 45),); assert!(matches!( parse_color("rgb(255, 260, 45)"), Err(ParseColorError::Unrecognized), )); assert!(matches!( parse_color("gray(25)"), Err(ParseColorError::InvalidGreyLevel { level: 25 }), )); assert_eq!( parse_color("gray(11)").unwrap(), parse_color("GREY(11)").unwrap(), ); } termimad-0.29.4/src/parse/parse_compound_style.rs000064400000000000000000000044701046102023000202340ustar 00000000000000use { super::*, crate::{ CompoundStyle, ATTRIBUTES, }, }; /// Read a Minimad CompoundStyle from a string. /// /// May contain attributes and from 0 to 2 colors, the first encountered /// one being the foreground. /// /// Examples: /// * `#ab00c1 strikeout` /// * `"red yellow bold italic"` /// * `""` /// * `"gray(2) gray(20)"` /// * `""` pub fn parse_compound_style(s: &str) -> Result { let tokens = parse_style_tokens(s)?; Ok(tokens.as_slice().into()) } impl From<&[StyleToken]> for CompoundStyle { fn from(tokens: &[StyleToken]) -> Self { let mut style = Self::default(); // first encountered color or None is considered as the foreground // and the following one(s) as the background let mut fg_set = false; for token in tokens { match token { StyleToken::Color(c) => { if fg_set { style.set_bg(*c); } else { style.set_fg(*c); fg_set = true; } } StyleToken::None => { if !fg_set { fg_set = true; } } StyleToken::Attribute(attribute) => { style.add_attr(*attribute); } StyleToken::Char(_) => { // not of use for compound styles } StyleToken::Align(_) => { // not of use for compound styles } StyleToken::Dimension(_) => { // not of use for compound styles } } } style } } impl PushStyleTokens for CompoundStyle { fn push_style_tokens(&self, tokens: &mut Vec) { if let Some(fg) = self.get_fg() { tokens.push(StyleToken::Color(fg)); } else if self.get_bg().is_some() { tokens.push(StyleToken::None); } if let Some(bg) = self.get_bg() { tokens.push(StyleToken::Color(bg)); } for &attr in ATTRIBUTES { if self.has_attr(attr) { tokens.push(StyleToken::Attribute(attr)); } } } } termimad-0.29.4/src/parse/parse_line_style.rs000064400000000000000000000032241046102023000173330ustar 00000000000000use { super::*, crate::LineStyle, }; /// Read a line_style from a string. pub fn parse_line_style(s: &str) -> Result { let tokens = parse_style_tokens(s)?; Ok(tokens.as_slice().into()) } impl From<&[StyleToken]> for LineStyle { fn from(tokens: &[StyleToken]) -> Self { let compound_style = tokens.into(); let mut left_margin = None; let mut right_margin = None; let mut align = Default::default(); for token in tokens { match token { StyleToken::Align(a) => { align = *a; } StyleToken::Dimension(number) => { if left_margin.is_some() { right_margin = Some(*number); } else { left_margin = Some(*number); } } _ => {} } } let left_margin = left_margin.unwrap_or_default() as usize; let right_margin = right_margin.unwrap_or_default() as usize; LineStyle { compound_style, align, left_margin, right_margin, } } } impl PushStyleTokens for LineStyle { fn push_style_tokens(&self, tokens: &mut Vec) { self.compound_style.push_style_tokens(tokens); tokens.push(StyleToken::Align(self.align)); if self.left_margin > 0 || self.right_margin > 0 { tokens.push(StyleToken::Dimension(self.left_margin.min(65536) as u16)); tokens.push(StyleToken::Dimension(self.right_margin.min(65536) as u16)); } } } termimad-0.29.4/src/parse/parse_styled_char.rs000064400000000000000000000013621046102023000174660ustar 00000000000000use { super::*, crate::StyledChar, }; /// Read a styled char from a string. pub fn parse_styled_char( s: &str, default_nude_char: char, ) -> Result { let tokens = parse_style_tokens(s)?; let style = tokens.as_slice().into(); let nude_char = tokens .iter() .find_map(|token| match token { StyleToken::Char(c) => Some(*c), _ => None, }) .unwrap_or(default_nude_char); Ok(StyledChar::new(style, nude_char)) } impl PushStyleTokens for StyledChar { fn push_style_tokens(&self, tokens: &mut Vec) { tokens.push(StyleToken::Char(self.nude_char())); self.compound_style().push_style_tokens(tokens); } } termimad-0.29.4/src/rect.rs000064400000000000000000000063741046102023000136260ustar 00000000000000use { crate::{ crossterm::{ cursor, QueueableCommand, }, Area, CompoundStyle, errors::Result, SPACE_FILLING, }, std::io::Write, }; #[derive(Debug)] pub struct RectBorderStyle { top_left: char, top_right: char, bottom_right: char, bottom_left: char, top: char, right: char, bottom: char, left: char, } pub static BORDER_STYLE_HALF_WIDTH_OUTSIDE: &RectBorderStyle = &RectBorderStyle { top_left: '▛', top: '▀', top_right: '▜', bottom: '▄', bottom_right: '▟', right: '▐', bottom_left: '▙', left: '▌', }; pub static BORDER_STYLE_MIDDLE_SQUARE_LINE: &RectBorderStyle = &RectBorderStyle { top_left: '┌', top: '─', top_right: '┐', bottom: '─', bottom_right: '┘', right: '│', bottom_left: '└', left: '│', }; pub static BORDER_STYLE_MIDDLE_ROUND_LINE: &RectBorderStyle = &RectBorderStyle { top_left: '╭', top: '─', top_right: '╮', bottom: '─', bottom_right: '╯', right: '│', bottom_left: '╰', left: '│', }; pub static BORDER_STYLE_BLAND: &RectBorderStyle = &RectBorderStyle { top_left: ' ', top: ' ', top_right: ' ', bottom: ' ', bottom_right: ' ', right: ' ', bottom_left: ' ', left: ' ', }; /// A drawable rect, with various types of borders and an optional /// filling. /// /// There may be more types of border in the future, if somebody /// asks for them #[derive(Debug)] pub struct Rect<'s> { pub area: Area, pub colors: CompoundStyle, pub fill: bool, pub border_style: &'s RectBorderStyle, } impl<'s> Rect<'s> { pub fn new(area: Area, colors: CompoundStyle) -> Self { Self { area, colors, fill: false, border_style: BORDER_STYLE_BLAND, } } pub fn set_border_style(&mut self, bs: &'s RectBorderStyle) { self.border_style = bs; } pub fn set_fill(&mut self, fill: bool) { self.fill = fill; } pub fn draw(&self, w: &mut W) -> Result<()> { let area = &self.area; let cs = &self.colors; let bs = &self.border_style; let mut y = area.top; w.queue(cursor::MoveTo(area.left, y))?; cs.queue(w, bs.top_left)?; if area.width > 2 { for _ in 0..area.width-2 { cs.queue(w, bs.top)?; } } cs.queue(w, bs.top_right)?; y += 1; while y < area.top + area.height - 1 { w.queue(cursor::MoveTo(area.left, y))?; cs.queue(w, bs.left)?; if self.fill { if area.width > 2 { SPACE_FILLING.queue_styled(w, cs, area.width as usize - 2)?; } } else { w.queue(cursor::MoveTo(area.left + area.width - 1, y))?; } cs.queue(w, bs.right)?; y += 1; } w.queue(cursor::MoveTo(area.left, area.bottom() - 1))?; cs.queue(w, bs.bottom_left)?; if area.width > 2 { for _ in 0..area.width-2 { cs.queue(w, bs.bottom)?; } } cs.queue(w, bs.bottom_right)?; Ok(()) } } termimad-0.29.4/src/scrollbar_style.rs000064400000000000000000000024111046102023000160600ustar 00000000000000use { crate::{ color::*, crossterm::style::Color, styled_char::StyledChar, }, }; /// A scrollbar style defined by two styled chars, one /// for the track, and one for the thumb. /// /// For the default styling only the fg color is defined /// and the char is ▐ but everything can be changed. #[derive(Clone, Debug, PartialEq)] pub struct ScrollBarStyle { pub track: StyledChar, pub thumb: StyledChar, } impl ScrollBarStyle { pub fn new() -> Self { let char = '▐'; Self { track: StyledChar::from_fg_char(gray(5), char), thumb: StyledChar::from_fg_char(gray(21), char), } } pub fn set_bg(&mut self, bg: Color) { self.track.set_bg(bg); self.thumb.set_bg(bg); } } impl Default for ScrollBarStyle { fn default() -> Self { Self::new() } } impl From for ScrollBarStyle { fn from(sc: StyledChar) -> Self { let char = sc.nude_char(); Self { track: StyledChar::from_fg_char( sc.get_bg().unwrap_or(gray(5)), char, ), thumb: StyledChar::from_fg_char( sc.get_fg().unwrap_or(gray(21)), char, ), } } } termimad-0.29.4/src/serde/mod.rs000064400000000000000000000002411046102023000145350ustar 00000000000000mod serde_skin; mod serde_compound_style; mod serde_line_style; mod serde_scrollbar_style; mod serde_styled_char; pub use { serde_scrollbar_style::*, }; termimad-0.29.4/src/serde/serde_compound_style.rs000064400000000000000000000012221046102023000202040ustar 00000000000000use { crate::{ CompoundStyle, parse_compound_style, parse::PushStyleTokens, }, serde::{de, Serialize, Serializer}, }; impl<'de> de::Deserialize<'de> for CompoundStyle { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { let s = String::deserialize(deserializer)?; parse_compound_style(&s).map_err(de::Error::custom) } } impl Serialize for CompoundStyle { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.to_style_tokens_string()) } } termimad-0.29.4/src/serde/serde_line_style.rs000064400000000000000000000011761046102023000173170ustar 00000000000000use { crate::{ LineStyle, parse_line_style, parse::PushStyleTokens, }, serde::{de, Serialize, Serializer}, }; impl<'de> de::Deserialize<'de> for LineStyle { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { let s = String::deserialize(deserializer)?; parse_line_style(&s).map_err(de::Error::custom) } } impl Serialize for LineStyle { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.to_style_tokens_string()) } } termimad-0.29.4/src/serde/serde_scrollbar_style.rs000064400000000000000000000027521046102023000203540ustar 00000000000000use { crate::{ CompoundStyle, ScrollBarStyle, StyledChar, }, serde::{ Deserialize, Serialize, }, }; /// A variable-complexity definition of a scrollbar, /// allowing a simplified representation covering most /// cases. /// /// You should not use this enum unless you're writing /// your own skin type Serialize/Deserialize impls. #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum ScrollBarStyleDef { Simple(StyledChar), Rich { track: StyledChar, thumb: StyledChar, }, } impl From<&ScrollBarStyle> for ScrollBarStyleDef { fn from(sc: &ScrollBarStyle) -> Self { let simple = sc.track.nude_char() == sc.thumb.nude_char() && sc.track.get_bg().is_none() && sc.thumb.get_bg().is_none(); if simple { Self::Simple(StyledChar::new( CompoundStyle::new( sc.thumb.get_fg(), sc.track.get_fg(), Default::default(), ), sc.track.nude_char(), )) } else { Self::Rich { track: sc.track.clone(), thumb: sc.thumb.clone(), } } } } impl ScrollBarStyleDef { pub fn into_scrollbar_style(self) -> ScrollBarStyle { match self { Self::Simple(sc) => sc.into(), Self::Rich{ track, thumb } => ScrollBarStyle { track, thumb }, } } } termimad-0.29.4/src/serde/serde_skin.rs000064400000000000000000000252151046102023000161140ustar 00000000000000use { super::ScrollBarStyleDef, crate::{ minimad::Alignment, parse_compound_style, parse_line_style, parse_styled_char, LineStyle, MadSkin, TableBorderChars, ATTRIBUTES, }, serde::{ de, ser::SerializeMap, Deserialize, Serialize, Serializer, }, std::fmt, }; impl<'de> de::Deserialize<'de> for MadSkin { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { struct SkinVisitor; impl<'de> de::Visitor<'de> for SkinVisitor { type Value = MadSkin; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("MadSkin") } fn visit_map(self, mut map: V) -> Result where V: de::MapAccess<'de>, { let mut skin = MadSkin::default(); while let Some(key) = map.next_key::()? { match key.as_str() { // inline styles "bold" => { let value = map.next_value::()?; let cs = parse_compound_style(&value).map_err(de::Error::custom)?; skin.bold = cs; } "italic" => { let value = map.next_value::()?; let cs = parse_compound_style(&value).map_err(de::Error::custom)?; skin.italic = cs; } "strikeout" => { let value = map.next_value::()?; let cs = parse_compound_style(&value).map_err(de::Error::custom)?; skin.strikeout = cs; } "inline_code" | "inline-code" => { let value = map.next_value::()?; let cs = parse_compound_style(&value).map_err(de::Error::custom)?; skin.inline_code = cs; } "ellipsis" => { let value = map.next_value::()?; let cs = parse_compound_style(&value).map_err(de::Error::custom)?; skin.ellipsis = cs; } // marker chars "bullet" => { let value = map.next_value::()?; let sc = parse_styled_char(&value, '*').map_err(de::Error::custom)?; skin.bullet = sc; } "quote_mark" | "quote" | "quote-mark" => { let value = map.next_value::()?; let sc = parse_styled_char(&value, '*').map_err(de::Error::custom)?; skin.quote_mark = sc; } "horizontal_rule" | "horizontal-rule" | "rule" => { let value = map.next_value::()?; let sc = parse_styled_char(&value, '*').map_err(de::Error::custom)?; skin.horizontal_rule = sc; } // scrollbar "scrollbar" => { let def: ScrollBarStyleDef = map.next_value()?; skin.scrollbar = def.into_scrollbar_style(); } // line styles "paragraph" => { let value = map.next_value::()?; let ls = parse_line_style(&value).map_err(de::Error::custom)?; skin.paragraph = ls; } "code_block" | "code-block" => { let value = map.next_value::()?; let ls = parse_line_style(&value).map_err(de::Error::custom)?; skin.code_block = ls; } "table" => { let value = map.next_value::()?; let ls = parse_line_style(&value).map_err(de::Error::custom)?; skin.table = ls; } // headers "headers" => match map.next_value::()? { HeadersStyleInfo::Add(ls) => { for h in &mut skin.headers { if let Some(fg) = ls.compound_style.get_fg() { h.compound_style.set_fg(fg); } if let Some(bg) = ls.compound_style.get_bg() { h.compound_style.set_bg(bg); } for &attr in ATTRIBUTES { if ls.compound_style.has_attr(attr) { h.compound_style.add_attr(attr); } } if ls.align != Alignment::Unspecified { h.align = ls.align; } } } HeadersStyleInfo::Levels(mut vls) => { for (lvl, h) in vls.drain(..).enumerate() { if lvl < skin.headers.len() { skin.headers[lvl] = h; } } } }, // table border chars // There's currently no way to allow custom table border // chars. It would require a change in MadSkin: either // make it require a lifetime, or use a Cow for the border // chars "table_border_chars" | "table-border-chars" => { let key = map.next_value::()?; if let Some(chars) = TableBorderChars::by_key(&key) { skin.table_border_chars = chars; } } _ => { let _ = map.next_value::()?; println!("unknown key: {key}"); } } } Ok(skin) } } deserializer.deserialize_map(SkinVisitor {}) } } impl Serialize for MadSkin { fn serialize(&self, serializer: S) -> Result where S: Serializer, { let mut skin = serializer.serialize_map(None)?; // inline styles skin.serialize_entry("bold", &self.bold)?; skin.serialize_entry("italic", &self.italic)?; skin.serialize_entry("strikeout", &self.strikeout)?; skin.serialize_entry("inline_code", &self.inline_code)?; skin.serialize_entry("ellipsis", &self.ellipsis)?; // marker chars skin.serialize_entry("bullet", &self.bullet)?; skin.serialize_entry("quote", &self.quote_mark)?; skin.serialize_entry("horizontal_rule", &self.horizontal_rule)?; // scrollbar let def: ScrollBarStyleDef = (&self.scrollbar).into(); skin.serialize_entry("scrollbar", &def)?; // line styles skin.serialize_entry("paragraph", &self.paragraph)?; skin.serialize_entry("code_block", &self.code_block)?; skin.serialize_entry("table", &self.table)?; // headers skin.serialize_entry("headers", &self.headers)?; // table border chars // There's currently no way to allow custom if let Some(key) = self.table_border_chars.key() { skin.serialize_entry("table_border_chars", key)?; } skin.end() } } #[derive(Deserialize)] #[serde(untagged)] enum HeadersStyleInfo { Add(LineStyle), Levels(Vec), } /// Check that serializing a skin in JSON, then deserializing this /// JSON into a new skin, results in an identical skin. #[test] fn skin_json_roundtrip() { use { crate::{ crossterm::style::{ Attribute, Color::*, }, gray, rgb, StyledChar, ROUNDED_TABLE_BORDER_CHARS, }, pretty_assertions::assert_eq, }; let skin = MadSkin::default(); let serialized = serde_json::to_string_pretty(&skin).unwrap(); let deserialized = serde_json::from_str(&serialized).unwrap(); assert_eq!(skin, deserialized); let mut skin = MadSkin::no_style(); skin.limit_to_ascii(); let serialized = serde_json::to_string_pretty(&skin).unwrap(); let deserialized = serde_json::from_str(&serialized).unwrap(); assert_eq!(skin, deserialized); let skin = MadSkin::default_dark(); let serialized = serde_json::to_string_pretty(&skin).unwrap(); let deserialized = serde_json::from_str(&serialized).unwrap(); assert_eq!(skin, deserialized); let skin = MadSkin::default_light(); let serialized = serde_json::to_string_pretty(&skin).unwrap(); let deserialized = serde_json::from_str(&serialized).unwrap(); assert_eq!(skin, deserialized); let mut skin = MadSkin::default(); skin.set_headers_fg(AnsiValue(178)); skin.headers[2].set_fg(gray(22)); skin.bold.set_fg(Yellow); skin.italic.set_fgbg(Magenta, rgb(30, 30, 40)); skin.bullet = StyledChar::from_fg_char(Yellow, '⟡'); skin.quote_mark.set_fg(Yellow); skin.italic.set_fg(Magenta); skin.scrollbar.thumb.set_fg(AnsiValue(178)); skin.table_border_chars = ROUNDED_TABLE_BORDER_CHARS; skin.paragraph.align = Alignment::Center; skin.table.align = Alignment::Center; skin.inline_code.add_attr(Attribute::Reverse); skin.paragraph.set_fgbg(Magenta, rgb(30, 30, 40)); skin.italic.add_attr(Attribute::Underlined); skin.italic.add_attr(Attribute::OverLined); let serialized = serde_json::to_string_pretty(&skin).unwrap(); let deserialized = serde_json::from_str(&serialized).unwrap(); assert_eq!(skin, deserialized); } termimad-0.29.4/src/serde/serde_styled_char.rs000064400000000000000000000012101046102023000174360ustar 00000000000000use { crate::{ StyledChar, parse_styled_char, parse::PushStyleTokens, }, serde::{de, Serialize, Serializer}, }; impl<'de> de::Deserialize<'de> for StyledChar { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de>, { let s = String::deserialize(deserializer)?; parse_styled_char(&s, '*').map_err(de::Error::custom) } } impl Serialize for StyledChar { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.to_style_tokens_string()) } } termimad-0.29.4/src/skin.rs000064400000000000000000000567661046102023000136470ustar 00000000000000use { crate::{ *, crossterm::{ queue, style::{ Attribute, Color, Print, }, }, errors::Result, table_border_chars::*, tbl::*, }, minimad::{ Alignment, Composite, Compound, Line, OwningTemplateExpander, TextTemplate, TextTemplateExpander, MAX_HEADER_DEPTH, }, std::{ fmt, io::Write, }, unicode_width::UnicodeWidthStr, }; /// A skin defining how a parsed markdown appears on the terminal /// (fg and bg colors, bold, italic, underline, etc.) #[derive(Clone, Debug, PartialEq)] pub struct MadSkin { pub paragraph: LineStyle, pub bold: CompoundStyle, pub italic: CompoundStyle, pub strikeout: CompoundStyle, pub inline_code: CompoundStyle, pub code_block: LineStyle, pub headers: [LineStyle; MAX_HEADER_DEPTH], pub scrollbar: ScrollBarStyle, pub table: LineStyle, // the compound style is for border chars pub bullet: StyledChar, pub quote_mark: StyledChar, pub horizontal_rule: StyledChar, pub ellipsis: CompoundStyle, pub table_border_chars: &'static TableBorderChars, pub list_items_indentation_mode: ListItemsIndentationMode, /// compounds which should be replaced with special /// renders. /// Experimental. This API will probably change /// (comments welcome) /// Do not use compounds with a length different than 1. #[cfg(feature = "special-renders")] pub special_chars: std::collections::HashMap, StyledChar>, } impl Default for MadSkin { /// Build a customizable skin. /// /// It's initialized with sensible gray level settings which should work /// whatever the terminal colors. /// /// If you want a default skin and you already know if your terminal /// is light or dark, you may use [MadSkin::default_light] /// or [MadSkin::default_dark]. fn default() -> Self { let mut skin = Self { paragraph: LineStyle::default(), bold: CompoundStyle::with_attr(Attribute::Bold), italic: CompoundStyle::with_attr(Attribute::Italic), strikeout: CompoundStyle::with_attr(Attribute::CrossedOut), inline_code: CompoundStyle::with_fgbg(gray(17), gray(3)), code_block: LineStyle::default(), headers: Default::default(), scrollbar: ScrollBarStyle::new(), table: CompoundStyle::with_fg(gray(7)).into(), bullet: StyledChar::from_fg_char(gray(8), '•'), quote_mark: StyledChar::new( CompoundStyle::new(Some(gray(12)), None, Attribute::Bold.into()), '▐', ), horizontal_rule: StyledChar::from_fg_char(gray(6), '―'), ellipsis: CompoundStyle::default(), table_border_chars: STANDARD_TABLE_BORDER_CHARS, list_items_indentation_mode: Default::default(), #[cfg(feature = "special-renders")] special_chars: std::collections::HashMap::new(), }; skin.code_block.set_fgbg(gray(17), gray(3)); for h in &mut skin.headers { h.add_attr(Attribute::Underlined); } skin.headers[0].add_attr(Attribute::Bold); skin.headers[0].align = Alignment::Center; skin } } impl MadSkin { /// Build a customizable skin with no style, most useful /// when your application must run in no-color mode, for /// example when piped to a file. /// /// Note that without style you have no underline, no /// strikeout, etc. pub fn no_style() -> Self { Self { paragraph: LineStyle::default(), bold: CompoundStyle::default(), italic: CompoundStyle::default(), strikeout: CompoundStyle::default(), inline_code: CompoundStyle::default(), code_block: LineStyle::default(), headers: Default::default(), scrollbar: ScrollBarStyle::new(), table: LineStyle::default(), bullet: StyledChar::nude('•'), quote_mark: StyledChar::nude('▐'), horizontal_rule: StyledChar::nude('―'), ellipsis: CompoundStyle::default(), list_items_indentation_mode: Default::default(), #[cfg(feature = "special-renders")] special_chars: std::collections::HashMap::new(), table_border_chars: STANDARD_TABLE_BORDER_CHARS, } } /// Build a customizable skin with gray levels suitable when the terminal has /// a dark background /// /// To determine whether the terminal is in light mode, you may use /// the [terminal-light](https://docs.rs/terminal-light/) crate. pub fn default_dark() -> Self { let mut skin = Self::default(); skin.code_block.set_fgbg(gray(20), gray(5)); skin.inline_code.set_fgbg(gray(20), gray(5)); skin.headers[0].set_fg(gray(22)); skin.headers[1].set_fg(gray(21)); skin.headers[2].set_fg(gray(20)); skin } /// Build a customizable skin with gray levels suitable when the terminal has /// a light background /// /// To determine whether the terminal is in light mode, you may use /// the [terminal-light](https://docs.rs/terminal-light/) crate. pub fn default_light() -> Self { let mut skin = Self::default(); skin.code_block.set_fgbg(gray(3), gray(20)); skin.inline_code.set_fgbg(gray(4), gray(20)); skin.headers[0].set_fg(gray(0)); skin.headers[1].set_fg(gray(2)); skin.headers[2].set_fg(gray(4)); skin } /// Change the characters used for table borders, bullets, etc. /// to be in the non extended ASCII range pub fn limit_to_ascii(&mut self) { self.table_border_chars = ASCII_TABLE_BORDER_CHARS; self.bullet.set_char('*'); self.quote_mark.set_char('>'); self.horizontal_rule.set_char('-'); } /// Blend the foreground and background colors (if any) into the given dest color, /// with a weight in `[0..1]`. /// /// The `dest` color can be for example a [crossterm] color or a [coolor] one. /// A `weight` of 0 lets the skin unchanged. pub fn blend_with + Copy>(&mut self, color: C, weight: f32) { self.paragraph.compound_style.blend_with(color, weight); self.bold.blend_with(color, weight); self.italic.blend_with(color, weight); self.inline_code.blend_with(color, weight); self.code_block.blend_with(color, weight); self.table.compound_style.blend_with(color, weight); self.strikeout.blend_with(color, weight); for h in &mut self.headers { h.blend_with(color, weight); } self.bullet.blend_with(color, weight); self.quote_mark.blend_with(color, weight); self.horizontal_rule.blend_with(color, weight); self.ellipsis.blend_with(color, weight); } /// Change the foreground of most styles (the ones which commonly /// have a default or uniform baground, don't change code styles /// for example). /// /// This can be used either as a first step in skin customization /// or as the only change done to a default skin. pub fn set_fg(&mut self, fg: Color) { self.paragraph.compound_style.set_fg(fg); self.bold.set_fg(fg); self.italic.set_fg(fg); self.strikeout.set_fg(fg); self.set_headers_fg(fg); self.bullet.set_fg(fg); self.quote_mark.set_fg(fg); self.horizontal_rule.set_fg(fg); self.ellipsis.set_fg(fg); #[cfg(feature = "special-renders")] { for (_, sc) in self.special_chars.iter_mut() { sc.set_fg(fg); } } } /// Change the background of most styles (the ones which commonly /// have a default or uniform baground, don't change code styles /// for example). /// /// This can be used either as a first step in skin customization /// or as the only change done to a default skin. pub fn set_bg(&mut self, bg: Color) { self.paragraph.compound_style.set_bg(bg); self.bold.set_bg(bg); self.italic.set_bg(bg); self.strikeout.set_bg(bg); self.set_headers_bg(bg); self.table.compound_style.set_bg(bg); self.bullet.set_bg(bg); self.quote_mark.set_bg(bg); self.horizontal_rule.set_bg(bg); self.ellipsis.set_bg(bg); self.scrollbar.set_bg(bg); #[cfg(feature = "special-renders")] { for (_, sc) in self.special_chars.iter_mut() { sc.set_bg(bg); } } } /// Set a common foreground color for all header levels /// /// (it's still possible to change them individually with /// `skin.headers[i]`) pub fn set_headers_fg(&mut self, c: Color) { for h in &mut self.headers { h.set_fg(c); } } /// Set a common background color for all header levels /// /// (it's still possible to change them individually with /// `skin.headers[i]`) pub fn set_headers_bg(&mut self, c: Color) { for h in &mut self.headers { h.set_bg(c); } } /// set a common background for the paragraph, headers, /// rules, etc. pub fn set_global_bg(&mut self, c: Color) { self.set_headers_bg(c); self.paragraph.set_bg(c); self.horizontal_rule.set_bg(c); } /// Return the number of visible cells pub fn visible_composite_length( &self, kind: CompositeKind, compounds: &[Compound<'_>], ) -> usize { let compounds_width: usize = compounds.iter().map(|c| c.src.width()).sum(); (match kind { CompositeKind::ListItem(depth) => 2 + depth as usize, // space and bullet CompositeKind::ListItemFollowUp(depth) => match self.list_items_indentation_mode { ListItemsIndentationMode::FirstLineOnly => 0, ListItemsIndentationMode::Block => 2 + depth as usize, // spaces }, CompositeKind::Quote => 2, // space of the quoting char _ => 0, }) + compounds_width } // FIXME deprecate ? pub fn visible_line_length(&self, line: &Line<'_>) -> usize { match line { Line::Normal(composite) => self.visible_composite_length(composite.style.into(), &composite.compounds), _ => 0, // FIXME implement } } /// return the style to apply to a given line pub const fn line_style(&self, kind: CompositeKind) -> &LineStyle { match kind { CompositeKind::Code => &self.code_block, CompositeKind::Header(level) if level <= MAX_HEADER_DEPTH as u8 => { &self.headers[level as usize - 1] } _ => &self.paragraph, } } /// return the style appliable to a given compound. /// It's a composition of the various appliable base styles. fn compound_style(&self, line_style: &LineStyle, compound: &Compound<'_>) -> CompoundStyle { if *compound.src == *crate::fit::ELLIPSIS { return self.ellipsis.clone(); } let mut os = line_style.compound_style.clone(); if compound.italic { os.overwrite_with(&self.italic); } if compound.strikeout { os.overwrite_with(&self.strikeout); } if compound.bold { os.overwrite_with(&self.bold); } if compound.code { os.overwrite_with(&self.inline_code); } os } /// return a formatted line or part of line. /// /// Don't use this function if `src` is expected to be several lines. pub fn inline<'k, 's>(&'k self, src: &'s str) -> FmtInline<'k, 's> { let composite = FmtComposite::from(Composite::from_inline(src), self); FmtInline { skin: self, composite, } } /// return a formatted text. /// /// Code blocs will be right justified pub fn text<'k, 's>(&'k self, src: &'s str, width: Option) -> FmtText<'k, 's> { FmtText::from(self, src, width) } /// return a formatted text, with lines wrapped or justified for the current terminal /// width. /// /// Code blocs will be right justified pub fn term_text<'k, 's>(&'k self, src: &'s str) -> FmtText<'k, 's> { let (width, _) = terminal_size(); FmtText::from(self, src, Some(width as usize)) } /// return a formatted text, with lines wrapped or justified for the /// passed area width (with space for a scrollbar). /// /// Code blocs will be right justified pub fn area_text<'k, 's>(&'k self, src: &'s str, area: &Area) -> FmtText<'k, 's> { FmtText::from(self, src, Some(area.width as usize - 1)) } pub fn write_in_area(&self, markdown: &str, area: &Area) -> Result<()> { let mut w = std::io::stdout(); self.write_in_area_on(&mut w, markdown, area)?; w.flush()?; Ok(()) } /// queue the rendered markdown in the specified area, without flush pub fn write_in_area_on(&self, w: &mut W, markdown: &str, area: &Area) -> Result<()> { let text = self.area_text(markdown, area); let mut view = TextView::from(area, &text); view.show_scrollbar = false; view.write_on(w) } /// do a `print!` of the given src interpreted as a markdown span /// /// Don't use this function if the string is expected to be several /// lines or have typed lines (titles, bullets, code fences, etc.): /// use `print_text` instead. pub fn print_inline(&self, src: &str) { print!("{}", self.inline(src)); } /// do a `print!` of the given src interpreted as a markdown text pub fn print_text(&self, src: &str) { print!("{}", self.term_text(src)); } /// do a `print!` of the given expander pub fn print_expander(&self, expander: TextTemplateExpander<'_, '_>) { let (width, _) = terminal_size(); let text = expander.expand(); let fmt_text = FmtText::from_text(self, text, Some(width as usize)); print!("{}", fmt_text); } /// do a `print!` of the given owning expander pub fn print_owning_expander( &self, expander: &OwningTemplateExpander<'_>, template: &TextTemplate<'_>, ) { let (width, _) = terminal_size(); let text = expander.expand(template); let fmt_text = FmtText::from_text(self, text, Some(width as usize)); print!("{}", fmt_text); } /// do a `print!` of the given owning expander pub fn print_owning_expander_md>( &self, expander: &OwningTemplateExpander<'_>, template: T, ) { let (width, _) = terminal_size(); let template_md: String = template.into(); let template = TextTemplate::from(&*template_md); let text = expander.expand(&template); let fmt_text = FmtText::from_text(self, text, Some(width as usize)); print!("{}", fmt_text); } pub fn print_composite(&self, composite: Composite<'_>) { print!( "{}", FmtInline { skin: self, composite: FmtComposite::from(composite, self), } ); } pub fn write_composite(&self, w: &mut W, composite: Composite<'_>) -> Result<()> where W: std::io::Write, { Ok(queue!( w, Print(FmtInline { skin: self, composite: FmtComposite::from(composite, self), }) )?) } /// write a composite filling the given width /// /// Ellision or truncation may occur, but no wrap. /// Use Alignement::Unspecified for a smart internal /// ellision pub fn write_composite_fill( &self, w: &mut W, composite: Composite<'_>, width: usize, align: Alignment, ) -> Result<()> where W: std::io::Write, { let mut fc = FmtComposite::from(composite, self); fc.fill_width(width, align, self); Ok(queue!( w, Print(FmtInline { skin: self, composite: fc, }) )?) } /// parse the given src as a markdown snippet and write it on /// the given `Write` pub fn write_inline_on(&self, w: &mut W, src: &str) -> Result<()> { Ok(queue!(w, Print(self.inline(src)))?) } /// parse the given src as a markdown text and write it on /// the given `Write` pub fn write_text_on(&self, w: &mut W, src: &str) -> Result<()> { Ok(queue!(w, Print(self.term_text(src)))?) } /// parse the given src as a markdown snippet and write it on stdout pub fn write_inline(&self, src: &str) -> Result<()> { let mut w = std::io::stdout(); self.write_inline_on(&mut w, src)?; w.flush()?; Ok(()) } /// parse the given src as a markdown text and write it on stdout pub fn write_text(&self, src: &str) -> Result<()> { let mut w = std::io::stdout(); self.write_text_on(&mut w, src)?; w.flush()?; Ok(()) } /// Write a composite. /// /// This function is internally used and normally not needed outside /// of Termimad's implementation. Its arguments may change. pub fn write_fmt_composite( &self, f: &mut fmt::Formatter<'_>, fc: &FmtComposite<'_>, outer_width: Option, with_right_completion: bool, with_margins: bool, ) -> fmt::Result { let ls = self.line_style(fc.kind); let (left_margin, right_margin) = if with_margins { ls.margins_in(outer_width) } else { (0, 0) }; let (lpi, rpi) = fc.completions(); // inner completion let inner_width = fc.spacing.map_or(fc.visible_length, |sp| sp.width); let (lpo, rpo) = Spacing::optional_completions( ls.align, inner_width + left_margin + right_margin, outer_width, ); self.paragraph.repeat_space(f, lpo + left_margin)?; ls.compound_style.repeat_space(f, lpi)?; if let CompositeKind::ListItem(depth) = fc.kind { for _ in 0..depth { write!(f, "{}", self.paragraph.compound_style.apply_to(' '))?; } write!(f, "{}", self.bullet)?; write!(f, "{}", self.paragraph.compound_style.apply_to(' '))?; } if self.list_items_indentation_mode == ListItemsIndentationMode::Block { if let CompositeKind::ListItemFollowUp(depth) = fc.kind { for _ in 0..depth+1 { write!(f, "{}", self.paragraph.compound_style.apply_to(' '))?; } write!(f, "{}", self.paragraph.compound_style.apply_to(' '))?; } } if fc.kind == CompositeKind::Quote { write!(f, "{}", self.quote_mark)?; write!(f, "{}", self.paragraph.compound_style.apply_to(' '))?; } #[cfg(feature = "special-renders")] for c in &fc.compounds { if let Some(replacement) = self.special_chars.get(c) { write!(f, "{}", replacement)?; } else { let os = self.compound_style(ls, c); write!(f, "{}", os.apply_to(c.as_str()))?; } } #[cfg(not(feature = "special-renders"))] for c in &fc.compounds { let os = self.compound_style(ls, c); write!(f, "{}", os.apply_to(c.as_str()))?; } ls.compound_style.repeat_space(f, rpi)?; if with_right_completion { self.paragraph.repeat_space(f, rpo + right_margin)?; } Ok(()) } /// Write a line in the passed formatter, with completions. /// /// Right completion is optional because: /// - if a text isn't right completed it shrinks better when you reduce the width /// of the terminal /// - right completion is useful to overwrite previous rendering without /// flickering (in scrollable views) pub fn write_fmt_line( &self, f: &mut fmt::Formatter<'_>, line: &FmtLine<'_>, width: Option, with_right_completion: bool, ) -> fmt::Result { let tbc = &self.table_border_chars; match line { FmtLine::Normal(fc) => { self.write_fmt_composite(f, fc, width, with_right_completion, true)?; } FmtLine::TableRow(FmtTableRow { cells }) => { let tbl_width = 1 + cells.iter().fold(0, |sum, cell| { if let Some(spacing) = cell.spacing { sum + spacing.width + 1 } else { sum + cell.visible_length + 1 } }); let (lpo, rpo) = Spacing::optional_completions(self.table.align, tbl_width, width); self.paragraph.repeat_space(f, lpo)?; for cell in cells { write!(f, "{}", self.table.compound_style.apply_to(tbc.vertical))?; self.write_fmt_composite(f, cell, None, false, false)?; } write!(f, "{}", self.table.compound_style.apply_to(tbc.vertical))?; if with_right_completion { self.paragraph.repeat_space(f, rpo)?; } } FmtLine::TableRule(rule) => { let tbl_width = 1 + rule.widths.iter().fold(0, |sum, w| sum + w + 1); let (lpo, rpo) = Spacing::optional_completions(self.table.align, tbl_width, width); self.paragraph.repeat_space(f, lpo)?; write!( f, "{}", self.table.compound_style.apply_to(match rule.position { RelativePosition::Top => tbc.top_left_corner, RelativePosition::Other => tbc.left_junction, RelativePosition::Bottom => tbc.bottom_left_corner, }) )?; for (idx, &width) in rule.widths.iter().enumerate() { if idx > 0 { write!( f, "{}", self.table.compound_style.apply_to(match rule.position { RelativePosition::Top => tbc.top_junction, RelativePosition::Other => tbc.cross, RelativePosition::Bottom => tbc.bottom_junction, }) )?; } self.table.repeat_char(f, tbc.horizontal, width)?; } write!( f, "{}", self.table.compound_style.apply_to(match rule.position { RelativePosition::Top => tbc.top_right_corner, RelativePosition::Other => tbc.right_junction, RelativePosition::Bottom => tbc.bottom_right_corner, }) )?; if with_right_completion { self.paragraph.repeat_space(f, rpo)?; } } FmtLine::HorizontalRule => { if let Some(w) = width { write!(f, "{}", self.horizontal_rule.repeated(w))?; } } } Ok(()) } } termimad-0.29.4/src/spacing.rs000064400000000000000000000053031046102023000143040ustar 00000000000000use { crate::{ compound_style::CompoundStyle, errors::Result, }, minimad::Alignment, }; #[derive(Debug, Clone, Copy)] pub struct Spacing { pub width: usize, pub align: Alignment, } fn truncate(s: &str, max_chars: usize) -> &str { match s.char_indices().nth(max_chars) { None => s, Some((idx, _)) => &s[..idx], } } impl Spacing { /// compute the number of chars to add left and write of inner_width /// to fill outer_width #[inline(always)] pub const fn completions( align: Alignment, inner_width: usize, outer_width: usize, ) -> (usize, usize) { if inner_width >= outer_width { return (0, 0); } match align { Alignment::Left | Alignment::Unspecified => (0, outer_width - inner_width), Alignment::Center => { let lp = (outer_width - inner_width) / 2; (lp, outer_width - inner_width - lp) } Alignment::Right => (outer_width - inner_width, 0), } } #[inline(always)] pub const fn optional_completions( align: Alignment, inner_width: usize, outer_width: Option, ) -> (usize, usize) { match outer_width { Some(outer_width) => Spacing::completions(align, inner_width, outer_width), None => (0, 0), } } #[inline(always)] pub const fn completions_for(&self, inner_width: usize) -> (usize, usize) { Spacing::completions(self.align, inner_width, self.width) } pub fn write_counted_str( &self, w: &mut W, s: &str, str_width: usize, style: &CompoundStyle, ) -> Result<()> where W: std::io::Write, { if str_width >= self.width { // we must truncate let s = truncate(s, self.width); style.queue_str(w, s)?; } else { // we must complete with spaces // This part could be written in a more efficient way let (lp, rp) = self.completions_for(str_width); let mut con = String::new(); for _ in 0..lp { con.push(' '); } con.push_str(s); for _ in 0..rp { con.push(' '); } style.queue(w, con)?; } Ok(()) } // FIXME use the number of chars instead of their real width, // the crop writer should be used when wide characters are expected pub fn write_str(&self, w: &mut W, s: &str, style: &CompoundStyle) -> Result<()> where W: std::io::Write, { self.write_counted_str(w, s, s.chars().count(), style) } } termimad-0.29.4/src/styled_char.rs000064400000000000000000000067461046102023000151750ustar 00000000000000use { crate::{ compound_style::CompoundStyle, crossterm::{ style::{ Color, PrintStyledContent, StyledContent, }, QueueableCommand, }, errors::Result, }, std::{ fmt::{ self, Display, }, io::Write, }, }; /// A modifiable character which can be easily written or repeated. Can /// be used for bullets, horizontal rules or quote marks. #[derive(Clone, Debug, PartialEq)] pub struct StyledChar { compound_style: CompoundStyle, nude_char: char, styled_char: StyledContent, // redundant, kept for performance } impl StyledChar { pub fn new(compound_style: CompoundStyle, nude_char: char) -> StyledChar { Self { nude_char, styled_char: compound_style.apply_to(nude_char), compound_style, } } pub fn nude(nude_char: char) -> StyledChar { Self::new(CompoundStyle::default(), nude_char) } pub fn nude_char(&self) -> char { self.nude_char } pub fn from_fg_char(fg: Color, nude_char: char) -> StyledChar { Self::new(CompoundStyle::with_fg(fg), nude_char) } /// Change the char, keeping colors and attributes pub fn set_char(&mut self, nude_char: char) { self.nude_char = nude_char; self.styled_char = self.compound_style.apply_to(self.nude_char); } pub const fn get_char(&self) -> char { self.nude_char } /// Change the fg color, keeping the char, bg color and attributes pub fn set_fg(&mut self, color: Color) { self.compound_style.set_fg(color); self.styled_char = self.compound_style.apply_to(self.nude_char); } pub const fn get_fg(&self) -> Option { self.compound_style.get_fg() } /// Change the bg color, keeping the char, fg color and attributes pub fn set_bg(&mut self, color: Color) { self.compound_style.set_bg(color); self.styled_char = self.compound_style.apply_to(self.nude_char); } pub const fn get_bg(&self) -> Option { self.compound_style.get_bg() } /// Change the style (colors, attributes) of the styled char pub fn set_compound_style(&mut self, compound_style: CompoundStyle) { self.compound_style = compound_style; self.styled_char = self.compound_style.apply_to(self.nude_char); } pub fn compound_style(&self) -> &CompoundStyle { &self.compound_style } /// Return a struct implementing `Display`, made of a (optimized) repetition /// of the character with its style. pub fn repeated(&self, count: usize) -> StyledContent { let mut s = String::new(); for _ in 0..count { s.push(self.nude_char); } self.compound_style.apply_to(s) } pub fn queue_repeat(&self, w: &mut W, count: usize) -> Result<()> { let mut s = String::new(); for _ in 0..count { s.push(self.nude_char); } self.compound_style.queue(w, s) } pub fn queue(&self, w: &mut W) -> Result<()> { w.queue(PrintStyledContent(self.styled_char))?; Ok(()) } pub fn blend_with>(&mut self, color: C, weight: f32) { self.compound_style.blend_with(color, weight); } } impl Display for StyledChar { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.styled_char.fmt(f) } } termimad-0.29.4/src/table_border_chars.rs000064400000000000000000000051131046102023000164630ustar 00000000000000 /// The set of characters to use to render table borders #[derive(Debug, Clone, PartialEq)] pub struct TableBorderChars { pub horizontal: char, pub vertical: char, pub top_left_corner: char, pub top_right_corner: char, pub bottom_right_corner: char, pub bottom_left_corner: char, pub top_junction: char, pub right_junction: char, pub bottom_junction: char, pub left_junction: char, pub cross: char, } impl TableBorderChars { /// return a key which could be used in by_key. /// /// (note that we're comparing the values, not the pointers) pub fn key(&self) -> Option<&'static str> { if self == STANDARD_TABLE_BORDER_CHARS { Some("standard") } else if self == ASCII_TABLE_BORDER_CHARS { Some("ascii") } else if self == ROUNDED_TABLE_BORDER_CHARS { Some("rounded") } else { None } } pub fn by_key(key: &str) -> Option<&'static Self> { match key { "standard" => Some(STANDARD_TABLE_BORDER_CHARS), "ascii" => Some(ASCII_TABLE_BORDER_CHARS), "rounded" => Some(ROUNDED_TABLE_BORDER_CHARS), _ => None, } } } /// Default square tables pub static STANDARD_TABLE_BORDER_CHARS: &TableBorderChars = &TableBorderChars { horizontal: '─', vertical: '│', top_left_corner: '┌', top_right_corner: '┐', bottom_right_corner: '┘', bottom_left_corner: '└', top_junction: '┬', right_junction: '┤', bottom_junction: '┴', left_junction: '├', cross: '┼', }; /// For tables made only of ASCII (not extended) /// /// It's automatically used when you call `skin.limit_to_ascii()` pub static ASCII_TABLE_BORDER_CHARS: &TableBorderChars = &TableBorderChars { horizontal: '-', vertical: '|', top_left_corner: '+', top_right_corner: '+', bottom_right_corner: '+', bottom_left_corner: '+', top_junction: '+', right_junction: '+', bottom_junction: '+', left_junction: '+', cross: '+', }; /// Allow tables to be more rounded /// /// ``` /// let mut skin = termimad::MadSkin::default(); /// skin.table_border_chars = termimad::ROUNDED_TABLE_BORDER_CHARS; /// ``` pub static ROUNDED_TABLE_BORDER_CHARS: &TableBorderChars = &TableBorderChars { horizontal: '─', vertical: '│', top_left_corner: '╭', top_right_corner: '╮', bottom_right_corner: '╯', bottom_left_corner: '╰', top_junction: '┬', right_junction: '┤', bottom_junction: '┴', left_junction: '├', cross: '┼', }; termimad-0.29.4/src/tbl.rs000064400000000000000000000215031046102023000134410ustar 00000000000000 use { crate::{ composite::*, line::FmtLine, skin::MadSkin, spacing::Spacing, fit::{TblFit, wrap}, }, minimad::{Alignment, TableRow}, }; /// Wrap a standard table row #[derive(Debug)] pub struct FmtTableRow<'s> { pub cells: Vec>, } /// Top, Bottom, or other #[derive(Debug)] pub enum RelativePosition { Top, Other, // or unknown Bottom, } /// A separator or alignment rule in a table. /// /// Represent this kind of lines in tables: /// |----|:-:|-- #[derive(Debug)] pub struct FmtTableRule { pub position: RelativePosition, // position relative to the table pub widths: Vec, pub aligns: Vec, } impl FmtTableRule { pub fn set_nbcols(&mut self, nbcols: usize) { self.widths.truncate(nbcols); self.aligns.truncate(nbcols); for ic in 0..nbcols { if ic >= self.widths.len() { self.widths.push(0); } if ic >= self.aligns.len() { self.aligns.push(Alignment::Unspecified); } } } } impl<'s> FmtTableRow<'s> { pub fn from(table_row: TableRow<'s>, skin: &MadSkin) -> FmtTableRow<'s> { let mut table_row = table_row; FmtTableRow { cells: table_row .cells .drain(..) .map(|composite| FmtComposite::from(composite, skin)) .collect(), } } } /// Tables are the sequences of lines whose line style is TableRow. /// /// A table is just the indices, without the text /// This structure isn't public because the indices are invalid as /// soon as rows are inserted. It only serves during the formatting /// process. struct Table { start: usize, height: usize, // number of lines nbcols: usize, // number of columns } #[allow(clippy::needless_range_loop)] impl Table { pub fn fix_columns( &mut self, lines: &mut Vec>, width: usize, skin: &MadSkin, ) { let mut nbcols = self.nbcols; if nbcols == 0 || width == 0 { return; } let mut cols_removed = false; // we add the missing cells and also prepare the fitter // We also add the missing cells let widths = match TblFit::new(nbcols, width) { Ok(mut tbl_fit) => { for line in lines.iter_mut().skip(self.start).take(self.height) { if let FmtLine::TableRow(FmtTableRow { cells }) = line { for ic in 0..nbcols { if cells.len() <= ic { cells.push(FmtComposite::new()); } else { tbl_fit.see_cell(ic, cells[ic].visible_length); } } } else if let FmtLine::TableRule(rule) = line { rule.set_nbcols(nbcols); } else { panic!("not a table row, should not happen"); } } tbl_fit.fit().col_widths } Err(_) => { // there's not enough width, we'll have to remove columns nbcols = (width - 1) / 4; cols_removed = true; vec![3; nbcols] } }; // At this step, all widths are at least 3 wide // Now we resize all cells and we insert new rows if necessary. // We iterate in reverse order so that we can insert rows // without recomputing row indices. for ir in (self.start..self.start + self.height).rev() { let line = &mut lines[ir]; if let FmtLine::TableRow(FmtTableRow { cells }) = line { let mut cells_to_add: Vec>> = Vec::new(); cells.truncate(nbcols); for ic in 0..nbcols { if cells.len() <= ic { //FIXME isn't this already done ? cells.push(FmtComposite::new()); continue; } cells_to_add.push(Vec::new()); if cells[ic].visible_length > widths[ic] { // we must wrap the cell over several lines let mut composites = wrap::hard_wrap_composite(&cells[ic], widths[ic], skin) .expect("tbl fitter guaranteed all columns to be wide enough"); // the first composite replaces the cell, while the other // ones go to cells_to_add let mut drain = composites.drain(..); cells[ic] = drain.next().unwrap(); for c in drain { cells_to_add[ic].push(c); } } } let nb_new_lines = cells_to_add.iter().fold(0, |m, cells| m.max(cells.len())); for inl in (0..nb_new_lines).rev() { let mut new_cells: Vec> = Vec::new(); for cell in cells_to_add.iter_mut().take(nbcols) { new_cells.push(if cell.len() > inl { cell.remove(inl) } else { FmtComposite::new() }); } let new_line = FmtLine::TableRow(FmtTableRow { cells: new_cells }); lines.insert(ir + 1, new_line); self.height += 1; } } } // Finally we iterate in normal order to specify alignment // (the alignments of a row are the ones of the last rule line) let mut current_aligns: Vec = vec![Alignment::Center; nbcols]; for ir in self.start..self.start + self.height { let line = &mut lines[ir]; match line { FmtLine::TableRow(FmtTableRow { cells }) => { for ic in 0..nbcols { cells[ic].spacing = Some(Spacing { width: widths[ic], align: current_aligns[ic], }); } } FmtLine::TableRule(rule) => { if cols_removed { rule.set_nbcols(nbcols); } if ir == self.start { rule.position = RelativePosition::Top; } else if ir == self.start + self.height - 1 { rule.position = RelativePosition::Bottom; } rule.widths[..nbcols].clone_from_slice(&widths[..nbcols]); current_aligns[..nbcols].clone_from_slice(&rule.aligns[..nbcols]); } _ => { panic!("It should be a table part"); } } } } } /// find the positions of all tables fn find_tables(lines: &[FmtLine<'_>]) -> Vec { let mut tables: Vec
= Vec::new(); let mut current: Option
= None; for (idx, line) in lines.iter().enumerate() { match line { FmtLine::TableRule(FmtTableRule { aligns, .. }) => match current.as_mut() { Some(b) => { b.height += 1; b.nbcols = b.nbcols.max(aligns.len()); } None => { current = Some(Table { start: idx, height: 1, nbcols: aligns.len(), }); } }, FmtLine::TableRow(FmtTableRow { cells }) => match current.as_mut() { Some(b) => { b.height += 1; b.nbcols = b.nbcols.max(cells.len()); } None => { current = Some(Table { start: idx, height: 1, nbcols: cells.len(), }); } }, _ => { if let Some(c) = current.take() { tables.push(c); } } } } if let Some(c) = current.take() { tables.push(c); } tables } /// Modify the rows of all tables in order to ensure it fits the widths /// and all cells have the widths of their column. /// /// Some lines may be added to the table in the process, which means any /// precedent indexing might be invalid. pub fn fix_all_tables( lines: &mut Vec>, width: usize, skin: &MadSkin, ) { for tbl in find_tables(lines).iter_mut().rev() { tbl.fix_columns(lines, width, skin); } } termimad-0.29.4/src/text.rs000064400000000000000000000060171046102023000136470ustar 00000000000000use { crate::{ code, line::FmtLine, skin::MadSkin, tbl, fit::wrap, }, minimad::{parse_text, Options, Text}, std::fmt, }; /// a formatted text, implementing Display. /// /// The text is wrapped for the width given at build, which /// means the rendering height is the number of lines. /// /// ``` /// use termimad::*; /// let skin = MadSkin::default(); /// let my_markdown = "#title\n* item 1\n* item 2"; /// let text = FmtText::from(&skin, &my_markdown, Some(80)); /// println!("{}", &text); /// ``` #[derive(Debug)] pub struct FmtText<'k, 's> { pub skin: &'k MadSkin, pub lines: Vec>, pub width: Option, // available width } impl<'k, 's> FmtText<'k, 's> { /// build a displayable text for the specified width and skin /// /// This can be called directly or using one of the skin helper /// method. pub fn from(skin: &'k MadSkin, src: &'s str, width: Option) -> FmtText<'k, 's> { let mt = parse_text(src, Options::default()); Self::from_text(skin, mt, width) } /// build a text as raw (with no markdown interpretation) pub fn raw_str(skin: &'k MadSkin, src: &'s str, width: Option) -> FmtText<'k, 's> { let mt = Text::raw_str(src); Self::from_text(skin, mt, width) } /// build a fmt_text from a minimad text pub fn from_text(skin: &'k MadSkin, mut text: Text<'s>, width: Option) -> FmtText<'k, 's> { let mut lines = text .lines .drain(..) .map(|mline| FmtLine::from(mline, skin)) .collect(); tbl::fix_all_tables(&mut lines, width.unwrap_or(std::usize::MAX), skin); code::justify_blocks(&mut lines); if let Some(width) = width { if width >= 3 { lines = wrap::hard_wrap_lines(lines, width, skin) .expect("width should be wide enough"); } } FmtText { skin, lines, width } } /// set the width to render the text to. /// /// It's preferable to set it no smaller than content_width and /// no wider than the terminal's width. /// /// If you want the text to be wrapped, pass a width on construction /// (ie in FmtText::from or FmtText::from_text) instead. /// The main purpose of this function is to optimize the rendering /// of a text (or several ones) to a content width, for example to /// have centered titles centered not based on the terminal's width /// but on the content width pub fn set_rendering_width(&mut self, width: usize) { self.width = Some(width); } pub fn content_width(&self) -> usize { self.lines .iter() .fold(0, |cw, line| cw.max(line.visible_length())) } } impl fmt::Display for FmtText<'_, '_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for line in &self.lines { self.skin.write_fmt_line(f, line, self.width, false)?; writeln!(f)?; } Ok(()) } } termimad-0.29.4/src/tokens.rs000064400000000000000000000034371046102023000141710ustar 00000000000000use { crate::*, minimad::*, unicode_width::UnicodeWidthChar, }; #[derive(Debug)] pub(crate) struct Token<'s> { pub compound: Compound<'s>, pub blank: bool, pub width: usize, pub start_in_compound: usize, pub end_in_compound: usize, } impl<'s> Token<'s> { pub fn to_compound(&self) -> Compound<'s> { let mut compound = self.compound.clone(); compound.set_str(&self.compound.src[self.start_in_compound..self.end_in_compound]); compound } } /// Cut a composite into token, each one being either only spaces or without space, and /// each one from one compound pub(crate) fn tokenize<'s, 'c>( compounds: &'c [Compound<'s>], max_token_width: usize, ) -> Vec> { let mut tokens: Vec> = Vec::new(); for compound in compounds { let mut token: Option = None; for (idx, char) in compound.src.char_indices() { let blank = char.is_whitespace() && !compound.code; let char_width = char.width().unwrap_or(0); if let Some(token) = token.as_mut() { if token.blank == blank && token.width + char_width <= max_token_width { token.width += char_width; token.end_in_compound += char.len_utf8(); continue; } } let new_token = Token { compound: compound.clone(), blank, width: char_width, start_in_compound: idx, end_in_compound: idx + char.len_utf8(), }; if let Some(token) = token.replace(new_token) { tokens.push(token); } } if let Some(token) = token { tokens.push(token); } } tokens } termimad-0.29.4/src/views/input_field.rs000064400000000000000000000541111046102023000163200ustar 00000000000000use { super::*, crate::{ crossterm::{ cursor, event::{ Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, }, queue, style::{ Attribute, Color, SetBackgroundColor, }, }, *, }, crokey::{OneToThree, KeyCombination, key}, std::io::Write, }; /// A simple input field, managing its cursor position and /// either handling the events you give it or being managed /// through direct manipulation functions /// (put_char, del_char_left, etc.). /// /// To create a multiline input_field (otherwise called a /// textarea) you should set an area with a height of more /// than 1 and allow newline to be created on keyboard with /// `new_line_on`. pub struct InputField { content: InputFieldContent, area: Area, focused_style: CompoundStyle, unfocused_style: CompoundStyle, cursor_style: CompoundStyle, /// when true, the display will have stars instead of the normal chars pub password_mode: bool, /// if not focused, the content will be displayed as text focused: bool, scroll: Pos, new_line_keys: Vec, } impl Default for InputField { fn default() -> Self { Self::new(Area::uninitialized()) } } macro_rules! wrap_content_fun { ($fun:ident) => { pub fn $fun(&mut self) -> bool { if self.content.$fun() { self.fix_scroll(); true } else { false } } }; } impl InputField { pub const ENTER: KeyCombination = key!(enter); pub const ALT_ENTER: KeyCombination = key!(alt-enter); pub fn new(area: Area) -> Self { let focused_style = CompoundStyle::default(); let unfocused_style = CompoundStyle::default(); let mut cursor_style = focused_style.clone(); cursor_style.add_attr(Attribute::Reverse); Self { content: InputFieldContent::default(), area, focused_style, unfocused_style, cursor_style, password_mode: false, focused: true, scroll: Pos::default(), new_line_keys: Vec::default(), } } pub fn set_mono_line(&mut self) { self.new_line_keys.clear(); } /// define a key which will be interpreted as a new line. /// /// You may define several ones. If you set none, the input /// field will stay monoline unless you manage key events /// yourself to insert new lines. /// /// Beware that keys like Ctrl-Enter and Shift-Enter /// are usually received by TUI applications as simple Enter. /// /// Example: /// ``` /// use termimad::*; /// let mut textarea = InputField::new(Area::new(5, 5, 20, 10)); /// textarea.new_line_on(InputField::ALT_ENTER); /// ``` pub fn new_line_on>(&mut self, key: K) { self.new_line_keys.push(key.into()); } /// Change the area x, y and width, but not the height. /// /// Makes most sense for monoline inputs pub fn change_area(&mut self, x: u16, y: u16, w: u16) { if self.area.left != x || self.area.top != y || self.area.width != w { self.area.left = x; self.area.top = y; self.area.width = w; self.fix_scroll(); } } pub fn set_area(&mut self, area: Area) { if self.area != area { self.area = area; self.fix_scroll(); } } pub const fn area(&self) -> &Area { &self.area } /// return the current scrolling state on both axis pub const fn scroll(&self) -> Pos { self.scroll } /// Tell the input to be or not focused pub fn set_focus(&mut self, b: bool) { if self.focused == b { return; } self.focused = b; // there's no reason to change the scroll when unfocusing if self.focused { self.fix_scroll(); } } pub const fn focused(&self) -> bool { self.focused } pub fn set_normal_style(&mut self, style: CompoundStyle) { self.focused_style = style; self.cursor_style = self.focused_style.clone(); self.cursor_style.add_attr(Attribute::Reverse); } pub fn set_unfocused_style(&mut self, style: CompoundStyle) { self.unfocused_style = style; } pub const fn content(&self) -> &InputFieldContent { &self.content } pub fn get_content(&self) -> String { self.content.to_string() } pub fn can_move_left(&self) -> bool { self.content.can_move_left() } pub fn can_move_right(&self) -> bool { self.content.can_move_right() } pub fn is_empty(&self) -> bool { self.content.is_empty() } pub fn copy_selection(&mut self) -> String { self.content.selection_string() } pub fn cut_selection(&mut self) -> String { let s = self.content.selection_string(); self.content.del_selection(); self.fix_scroll(); s } /// Write the given string in place of the selection, or /// insert the string if there's no wide selection. /// /// This is the usual behavior for pasting a string. pub fn replace_selection>(&mut self, s: S) { if self.content.has_wide_selection() { self.content.del_selection(); } self.content.insert_str(s); self.fix_scroll(); } /// tell whether the content of the input is equal /// to the argument pub fn is_content(&self, s: &str) -> bool { self.content.is_str(s) } /// change the content to the new one and /// put the cursor at the end **if** the /// content is different from the previous one. pub fn set_str>(&mut self, s: S) { self.content.set_str(s); self.fix_scroll(); } pub fn insert_new_line(&mut self) -> bool { self.content.insert_new_line(); self.fix_scroll(); true } /// put a char at cursor position (and increment this /// position). pub fn put_char(&mut self, c: char) -> bool { self.content.insert_char(c); self.fix_scroll(); true } pub fn clear(&mut self) { self.content.clear(); self.fix_scroll(); } /// Insert the string on cursor point, as if it was typed pub fn insert_str>(&mut self, s: S) { self.content.insert_str(s); self.fix_scroll(); } wrap_content_fun!(move_up); wrap_content_fun!(move_down); wrap_content_fun!(move_left); wrap_content_fun!(move_right); wrap_content_fun!(move_to_start); wrap_content_fun!(move_to_end); wrap_content_fun!(move_to_line_start); wrap_content_fun!(move_to_line_end); wrap_content_fun!(move_word_left); wrap_content_fun!(move_word_right); wrap_content_fun!(del_char_below); wrap_content_fun!(del_selection); wrap_content_fun!(del_char_left); wrap_content_fun!(del_word_left); wrap_content_fun!(del_word_right); wrap_content_fun!(move_current_line_up); wrap_content_fun!(move_current_line_down); wrap_content_fun!(select_word_around); pub fn page_up(&mut self) -> bool { if self.content.move_lines_up(self.area.height as usize) { self.fix_scroll(); true } else { false } } pub fn page_down(&mut self) -> bool { if self.content.move_lines_down(self.area.height as usize) { self.fix_scroll(); true } else { false } } /// apply an event being a key /// /// /// This function handles a few events like deleting a /// char, or going to the start (home key) or end (end key) /// of the input. If you want to totally handle events, you /// may call function like `put_char` and `del_char_left` /// directly. pub fn apply_key_event(&mut self, key: KeyEvent) -> bool { self.apply_key_combination(key) } /// apply a key combination /// /// /// This function handles a few events like deleting a /// char, or going to the start (home key) or end (end key) /// of the input. If you want to totally handle events, you /// may call function like `put_char` and `del_char_left` /// directly. pub fn apply_key_combination>( &mut self, key: K, ) -> bool { if !self.focused { return false; } let key = key.into(); if self.new_line_keys.contains(&key) { self.insert_new_line(); return true; } use crate::crossterm::event::KeyModifiers as Mod; match (key.codes, key.modifiers) { (OneToThree::One(code), Mod::NONE) => self.apply_keycode_event(code, false), (OneToThree::One(code), Mod::SHIFT) => self.apply_keycode_event(code, true), _ => false, } } /// apply an event being a key without modifier. /// /// You don't usually call this function but the more /// general `apply_event`. This one is useful when you /// manage events mostly yourselves. pub fn apply_keycode_event(&mut self, code: KeyCode, shift: bool) -> bool { if code == KeyCode::Backspace { if self.content.has_wide_selection() { self.content.del_selection(); true } else { self.content.del_char_left() } } else if code == KeyCode::Delete { if self.content.has_wide_selection() { self.content.del_selection(); true } else { self.content.del_char_below() } } else if let KeyCode::Char(c) = code { if self.content.has_wide_selection() { self.content.del_selection(); self.put_char(c); self.fix_scroll(); true } else { self.put_char(c) } } else { if shift { self.content.make_selection(); } else { self.content.unselect(); } match code { KeyCode::Home => self.move_to_line_start(), KeyCode::End => self.move_to_line_end(), KeyCode::Up => self.move_up(), KeyCode::Down => self.move_down(), KeyCode::Left => self.move_left(), KeyCode::PageUp => self.page_up(), KeyCode::PageDown => self.page_down(), KeyCode::Right => self.move_right(), _ => false, } } } /// Apply a simple left click event pub fn apply_click_event(&mut self, x: u16, y: u16) -> bool { if self.area.contains(x, y) { if self.focused { let y = ((y - self.area.top) as usize + self.scroll.y) .min(self.content.line_count() - 1); let line = &self.content.lines()[y]; let x = line .col_to_char_idx((x - self.area.left) as usize + self.scroll.x) .unwrap_or(line.chars.len()); self.content.set_cursor_pos(Pos { x, y }); } else { self.focused = true; } true } else { false } } /// Apply a mouse event pub fn apply_mouse_event(&mut self, mouse_event: MouseEvent, is_double_click: bool) -> bool { let MouseEvent { kind, column, row, modifiers, } = mouse_event; if self.area.contains(column, row) { if self.focused { let y = ((row - self.area.top) as usize + self.scroll.y) .min(self.content.line_count() - 1); let line = &self.content.lines()[y]; let x = line .col_to_char_idx((column - self.area.left) as usize + self.scroll.x) .unwrap_or(line.chars.len()); // We handle the selection click on down, so that it's set at the // start of drag. match kind { MouseEventKind::Down(MouseButton::Left) => { // FIXME Crossterm doesn't seem to send shift modifier // with up or down mouse events if modifiers == KeyModifiers::SHIFT { self.content.make_selection(); } else { self.content.unselect(); } self.content.set_cursor_pos(Pos { x, y }); } MouseEventKind::Up(MouseButton::Left) if is_double_click => { self.content.set_cursor_pos(Pos { x, y }); self.content.select_word_around(); } MouseEventKind::Drag(MouseButton::Left) => { self.content.make_selection(); self.content.set_cursor_pos(Pos { x, y }); } MouseEventKind::ScrollDown => { self.scroll_down(); } MouseEventKind::ScrollUp => { self.scroll_up(); } _ => {} } } else if matches!(kind, MouseEventKind::Down(MouseButton::Left)) { self.focused = true; } true } else { false } } /// apply the event to change the state (content, cursor) /// /// Return true when the event was used. pub fn apply_event(&mut self, event: &Event, is_double_click: bool) -> bool { match event { Event::Mouse(mouse_event) => self.apply_mouse_event(*mouse_event, is_double_click), Event::Key(KeyEvent { code, modifiers, .. }) if self.focused => { if modifiers.is_empty() { self.apply_keycode_event(*code, false) } else if *modifiers == KeyModifiers::SHIFT { self.apply_keycode_event(*code, true) } else { false } } _ => false, } } /// apply the event to change the state (content, cursor, focus) /// /// Return true when the event was used. pub fn apply_timed_event(&mut self, event: &TimedEvent) -> bool { self.apply_event(&event.event, event.double_click) } pub fn scroll_up(&mut self) -> bool { if self.scroll.y > 0 { self.scroll.y -= 1; true } else { false } } pub fn scroll_down(&mut self) -> bool { let height = self.area.height as usize; let lines_len = self.content.line_count(); if self.scroll.y + height < lines_len { self.scroll.y += 1; true } else { false } } fn fix_scroll(&mut self) { let mut width = self.area.width as usize; let height = self.area.height as usize; let lines = &self.content.lines(); let has_y_scroll = lines.len() > height; if has_y_scroll { width -= 1; } else { self.scroll.y = 0; } let pos = self.content.cursor_pos(); if has_y_scroll { if self.scroll.y + height > lines.len() { self.scroll.y = lines.len() - height; } if self.focused { // we must ensure the cursor is visible if self.scroll.y > pos.y { self.scroll.y = pos.y; if self.scroll.y > 0 && height > 4 { self.scroll.y -= 1; } } else if pos.y >= self.scroll.y + height { self.scroll.y = pos.y - height + 1; if pos.y + 1 < lines.len() { self.scroll.y -= 1; } } } } let line = self.content.current_line(); let line_width = line.width(); if line_width < width { self.scroll.x = 0; } else { if self.focused { // we don't show ellipsis if the width is below 4 // so we need less margin if width < 4 { if pos.x < 2 { self.scroll.x = 0; } else if pos.x < self.scroll.x + 1 { self.scroll.x = pos.x - 1; } else if pos.x > self.scroll.x + width { self.scroll.x = pos.x + 1 - width; } } else { let wpx = line.char_idx_to_col(pos.x); if wpx < self.scroll.x + 2 { if wpx < 2 { self.scroll.x = 0; } else { self.scroll.x = wpx - 2; } } else if wpx > self.scroll.x + width - 2 { self.scroll.x = wpx + 2 - width; } } } if self.scroll.x + width > line_width + 1 { self.scroll.x = line_width + 1 - width; } } } /// Render the input field on screen. /// /// All rendering must be explicitely called, no rendering is /// done on functions changing the state. /// /// w is typically either stderr or stdout. This function doesn't /// flush by itself (useful to avoid flickering) pub fn display_on(&self, w: &mut W) -> Result<(), Error> { let normal_style = if self.focused { &self.focused_style } else { &self.unfocused_style }; let mut width = self.area.width as usize; let pos = self.content.cursor_pos(); let scrollbar = self .area .scrollbar(self.scroll.y as u16, self.content.line_count() as u16); if scrollbar.is_some() { width -= 1; } queue!(w, SetBackgroundColor(Color::Reset))?; let mut scrollbar_style = &crate::get_default_skin().scrollbar; let mut focused_scrollbar_style; if self.focused { if let Some(bg) = self.focused_style.get_bg() { focused_scrollbar_style = scrollbar_style.clone(); focused_scrollbar_style.set_bg(bg); scrollbar_style = &focused_scrollbar_style; } } let mut numbered_lines = self .content .lines() .iter() .map(|line| &line.chars) .enumerate() .skip(self.scroll.y); let selection = self.content.selection(); for j in 0..self.area.height { queue!(w, cursor::MoveTo(self.area.left, j + self.area.top))?; if let Some((y, chars)) = numbered_lines.next() { let cursor_at_end = self.focused && y == pos.y && pos.x == chars.len(); let mut width_to_skip = self.scroll.x; let mut skipped_width = 0; let mut displayed_width = 0; let mut width = width; // available width for rendering chars // we don't show ellipsis if the width is 4 or less if width_to_skip > 0 && width > 4 { normal_style.queue(w, fit::ELLIPSIS)?; width_to_skip += 1; width -= 1; } for (i, c) in chars.iter().enumerate() { let c = if self.password_mode { '*' } else { *c }; let char_width = InputFieldContent::char_width(c); if skipped_width < width_to_skip { // char hidden by scroll on x skipped_width += char_width; continue; } if displayed_width + char_width >= width { let is_last = i == chars.len() - 1; if !is_last || displayed_width + char_width > width { if self.focused && selection.contains(i, y) { self.cursor_style.queue(w, fit::ELLIPSIS)?; } else { normal_style.queue(w, fit::ELLIPSIS)?; } displayed_width += 1; break; } } if self.focused && selection.contains(i, y) { self.cursor_style.queue(w, c)?; } else { normal_style.queue(w, c)?; } displayed_width += char_width; if displayed_width >= width { break; } } if displayed_width < width && cursor_at_end { self.cursor_style.queue(w, ' ')?; displayed_width += 1; } while displayed_width < width { normal_style.queue(w, ' ')?; displayed_width += 1; } } else { SPACE_FILLING.queue_styled(w, normal_style, width)?; } if let Some((sctop, scbottom)) = scrollbar { let y = j + self.area.top; if sctop <= y && y <= scbottom { scrollbar_style.thumb.queue(w)?; } else { scrollbar_style.track.queue(w)?; } } } Ok(()) } /// render the input field on stdout pub fn display(&self) -> Result<(), Error> { let mut w = std::io::stdout(); self.display_on(&mut w)?; w.flush()?; Ok(()) } } termimad-0.29.4/src/views/input_field_content.rs000064400000000000000000000550641046102023000200620ustar 00000000000000use { super::{Pos, Range}, crate::TAB_REPLACEMENT, std::{ fmt, }, unicode_width::UnicodeWidthChar, }; #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Line { pub chars: Vec, } /// an iterator over the chars of an InputFieldContent or /// of a selection pub struct Chars<'c> { content: &'c InputFieldContent, pos: Pos, end: Pos, } /// the content of an InputField. /// /// Doesn't know about rendering, styles, areas, etc. #[derive(Debug, Clone, PartialEq, Eq)] pub struct InputFieldContent { /// the cursor's position pos: Pos, /// the other end of the selection, if any selection_tail: Option, /// text lines, always at least one lines: Vec, } impl Iterator for Chars<'_> { type Item = char; fn next(&mut self) -> Option { if self.pos > self.end { return None; } let line = &self.content.lines[self.pos.y]; if self.pos.x < line.chars.len() { self.pos.x += 1; Some(line.chars[self.pos.x - 1]) } else if self.pos.y + 1 < self.content.lines.len() { self.pos.y += 1; self.pos.x = 0; Some('\n') } else { None } } } impl<'c> IntoIterator for &'c InputFieldContent { type Item = char; type IntoIter = Chars<'c>; fn into_iter(self) -> Self::IntoIter { Chars { content: self, pos: Pos::default(), end: self.end(), } } } impl fmt::Display for Line { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use fmt::Write; for &c in &self.chars { f.write_char(c)?; } Ok(()) } } impl Default for InputFieldContent { fn default() -> Self { Self { pos: Pos::default(), selection_tail: None, // there's always a line lines: vec![Line::default()], } } } impl fmt::Display for InputFieldContent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { use fmt::Write; let mut lines = self.lines.iter().peekable(); while let Some(line) = lines.next() { for &c in &line.chars { f.write_char(c)?; } if lines.peek().is_some() { f.write_char('\n')?; } } Ok(()) } } impl Line { pub fn col_to_char_idx(&self, col: usize) -> Option { let mut sum_widths = 0; for (idx, &c) in self.chars.iter().enumerate() { if col <= sum_widths { return Some(idx); } sum_widths += InputFieldContent::char_width(c); } None } pub fn char_idx_to_col(&self, idx: usize) -> usize { self.chars[0..idx].iter() .map(|&c| InputFieldContent::char_width(c)) .sum() } pub fn width(&self) -> usize { self.chars.iter().map(|&c| InputFieldContent::char_width(c)).sum() } } impl InputFieldContent { pub fn line_count(&self) -> usize { self.lines.len() } pub fn line(&self, y: usize) -> Option<&Line> { self.lines.get(y) } pub fn line_saturating(&self, y: usize) -> &Line { self.lines.get(y) .unwrap_or(&self.lines[self.lines.len()-1]) } pub fn current_line(&self) -> &Line { self.lines .get(self.pos.y) .expect("current line should exist") } pub fn lines(&self) -> &[Line] { &self.lines } pub const fn cursor_pos(&self) -> Pos { self.pos } /// Set the cursor position. /// /// The position set may be different to ensure consistency /// (for example if it's after the end, it will be set back). pub fn set_cursor_pos(&mut self, new_pos: Pos) { self.pos = self.make_valid_pos(new_pos); } /// Return the given pos, maybe modified to be valid for the content pub fn make_valid_pos(&self, mut pos: Pos) -> Pos { if pos.y >= self.lines.len() { self.end() } else { pos.x = pos.x.min(self.lines[pos.y].chars.len()); pos } } /// Set the selection tail to the current pos if there's no selection pub fn make_selection(&mut self) { if self.selection_tail.is_none() { self.selection_tail = Some(self.pos); } } pub fn unselect(&mut self) { self.selection_tail = None; } pub fn set_selection_tail(&mut self, sel_tail: Pos) { if sel_tail.y >= self.lines.len() { self.selection_tail = Some(self.end()); } else { self.selection_tail = Some(Pos { y: sel_tail.y, x: sel_tail.x.min(self.lines[self.pos.y].chars.len()), }); } } fn fix_pos(&mut self) { self.pos = self.make_valid_pos(self.pos); } fn fix_selection_tail(&mut self) { if let Some(sel_tail) = self.selection_tail { self.selection_tail = Some(self.make_valid_pos(sel_tail)); } } fn fix_selection(&mut self) { self.fix_pos(); self.fix_selection_tail(); } pub fn selection(&self) -> Range { if let Some(sel_tail) = self.selection_tail { if sel_tail < self.pos { Range { min: sel_tail, max: self.pos } } else { Range { min: self.pos, max: sel_tail } } } else { Range { min: self.pos, max: self.pos } } } /// return an iterator over the characters of the /// selection (including some newline chars maybe) pub fn selection_chars(&self) -> Chars<'_> { let Range { min, max } = self.selection(); Chars { content: self, pos: min, end: max, } } pub fn selection_string(&self) -> String { self.selection_chars().collect() } pub fn is_empty(&self) -> bool { match self.lines.len() { 1 => self.lines[0].chars.is_empty(), _ => false, } } pub const fn has_selection(&self) -> bool { self.selection_tail.is_some() } pub fn has_wide_selection(&self) -> bool { self.selection_tail.map_or(false, |sel_tail| sel_tail != self.pos) } /// return the position on end, where the cursor should be put /// initially pub fn end(&self) -> Pos { let y = self.lines.len() - 1; Pos { x:self.lines[y].chars.len(), y } } fn last_line(&mut self) -> &mut Line { let y = self.lines.len() - 1; &mut self.lines[y] } /// add a char at end, without updating the position. /// /// This shouldn't be used in normal event handling as /// characters are normally inserted on insertion point /// with insert_char. pub fn push_char(&mut self, c: char) { match c { '\n' => self.lines.push(Line::default()), '\r' | '\x08' /*backspacea*/ => {} _ => self.last_line().chars.push(c), } } /// Initialize from a string, with the cursor at end pub fn from>(s: S) -> Self { let mut content = Self::default(); content.insert_str(s); content } pub fn clear(&mut self) { self.lines.clear(); self.lines.push(Line::default()); self.pos = Pos::default(); self.selection_tail = None; } pub fn insert_new_line(&mut self) { let new_line = Line { chars: self.lines[self.pos.y].chars.split_off(self.pos.x), }; self.pos.x = 0; self.pos.y += 1; self.lines.insert(self.pos.y, new_line); self.fix_selection(); } /// Insert a character at the current position, updating /// this position pub fn insert_char(&mut self, c: char) { if c == '\n' { self.insert_new_line(); } else if c == '\r' || c == '\x08' { // skipping } else { self.lines[self.pos.y].chars.insert(self.pos.x, c); self.pos.x += 1; } } /// Insert the string on cursor point, as if it was typed pub fn insert_str>(&mut self, s: S) { for c in s.as_ref().chars() { self.insert_char(c); } } /// Tell whether the content of the input is equal to the argument, /// comparing char by char pub fn is_str(&self, s: &str) -> bool { let mut ia = self.into_iter(); let mut ib = s.chars(); loop { match (ia.next(), ib.next()) { (Some(a), Some(b)) if a == b => { continue } (None, None) => { return true; } _ => { return false; } } } } /// Change the content to the new one and put the cursor at the end **if** the /// content is different from the previous one. /// /// Don't move the cursor if the string content didn't change. pub fn set_str>(&mut self, s: S) { if self.is_str(s.as_ref()) { return; } self.clear(); self.insert_str(s); } /// Remove the char left of the cursor, if any. pub fn del_char_left(&mut self) -> bool { if self.pos.x > 0 { self.pos.x -= 1; if !self.lines[self.pos.y].chars.is_empty() { self.lines[self.pos.y].chars.remove(self.pos.x); } self.fix_selection(); true } else if self.pos.y > 0 && self.lines.len() > 1 { let mut removed_line = self.lines.remove(self.pos.y); self.pos.y -= 1; self.pos.x = self.lines[self.pos.y].chars.len(); self.lines[self.pos.y].chars.append(&mut removed_line.chars); self.fix_selection(); true } else { false } } /// make the word around the current pos, if any, the current selection pub fn select_word_around(&mut self) -> bool { let chars = &self.lines[self.pos.y].chars; let mut start = self.pos.x; if start >= chars.len() || !is_word_char(chars[start]) { return false; } while start > 0 && is_word_char(chars[start-1]) { start -= 1; } let mut end = self.pos.x; while end + 1 < chars.len() && is_word_char(chars[end+1]) { end += 1; } self.selection_tail = Some(Pos::new(start, self.pos.y)); self.pos.x = end; true } /// Remove the char at cursor position, if any. /// /// Cursor position is unchanged pub fn del_char_below(&mut self) -> bool { let line_len = self.current_line().chars.len(); if line_len == 0 { if self.lines.len() > 1 { self.lines.remove(self.pos.y); self.fix_selection(); true } else { false } } else if self.pos.x < line_len { self.lines[self.pos.y].chars.remove(self.pos.x); self.fix_selection(); true } else if self.lines.len() > self.pos.y + 1 { let mut removed_line = self.lines.remove(self.pos.y + 1); self.lines[self.pos.y].chars.append(&mut removed_line.chars); self.fix_selection(); true } else { false } } pub fn del_selection(&mut self) -> bool { let Range { min, max } = self.selection(); if min.y == max.y { if min.x == max.x { return self.del_char_below(); } if max.x == self.lines[min.y].chars.len() { if min.x == 0 { // we remove the whole line self.lines.drain(min.y..min.y+1); if self.lines.is_empty() { self.lines.push(Line::default()); } } else { self.lines[min.y].chars.drain(min.x..); } } else { self.lines[min.y].chars.drain(min.x..max.x+1); } } else { let min_y = if min.x > 0 { self.lines[min.y].chars.truncate(min.x); min.y + 1 } else { min.y }; let max_y = if max.x < self.lines[max.y].chars.len() { self.lines[max.y].chars.drain(0..max.x); max.y - 1 } else { max.y }; if max_y > min_y { self.lines.drain(min_y..(max_y+1).min(self.lines.len())); if self.lines.is_empty() { self.lines.push(Line::default()); } } } self.set_cursor_pos(min); self.selection_tail = None; true } /// Swap two lines. Return false if one of the indices is out of /// range or if the two indices are the same pub fn swap_lines(&mut self, ya: usize, yb: usize) -> bool { if ya != yb && ya < self.lines.len() && yb < self.lines.len() { self.lines.swap(ya, yb); self.fix_selection(); true } else { false } } /// Swap the current line with the line before, if possible pub fn move_current_line_up(&mut self) -> bool { if self.pos.y > 0 && self.swap_lines(self.pos.y - 1, self.pos.y) { self.pos.y -= 1; self.fix_selection(); true } else { false } } /// Swap the current line with the line after, if possible pub fn move_current_line_down(&mut self) -> bool { if self.swap_lines(self.pos.y + 1, self.pos.y) { self.pos.y += 1; self.fix_selection(); true } else { false } } /// Tell whether it's possible to move the cursor to the /// right (or to the next line) pub fn can_move_right(&self) -> bool { self.pos.x < self.lines[self.pos.y].chars.len() || self.pos.y < self.lines.len() - 1 } /// Move the cursor to the right (or to the line below /// if it's a the end of a non-last line) pub fn move_right(&mut self) -> bool { if self.pos.x < self.lines[self.pos.y].chars.len() { self.pos.x += 1; true } else if self.pos.y < self.lines.len() - 1 { self.pos.y += 1; self.pos.x = 0; true } else { false } } /// Move the cursor up pub fn move_lines_up(&mut self, lines: usize) -> bool { if self.pos.y > 0 { let cols = self.lines[self.pos.y].char_idx_to_col(self.pos.x); self.pos.y -= lines.min(self.pos.y); let line = &self.lines[self.pos.y]; self.pos.x = line.col_to_char_idx(cols).unwrap_or(line.chars.len()); true } else { false } } /// Move the cursor one line up pub fn move_up(&mut self) -> bool { self.move_lines_up(1) } /// Move the cursor down pub fn move_lines_down(&mut self, lines: usize) -> bool { if self.pos.y + 1 < self.lines.len() { let cols = self.lines[self.pos.y].char_idx_to_col(self.pos.x); self.pos.y += lines.min(self.lines.len() - self.pos.y - 1); let line = &self.lines[self.pos.y]; self.pos.x = line.col_to_char_idx(cols).unwrap_or(line.chars.len()); true } else { false } } pub fn move_down(&mut self) -> bool { self.move_lines_down(1) } pub fn can_move_left(&self) -> bool { self.pos.x > 0 || self.pos.y > 0 } pub fn move_left(&mut self) -> bool { if self.pos.x > 0 { self.pos.x -= 1; true } else if self.pos.y > 0 { self.pos.y -= 1; self.pos.x = self.lines[self.pos.y].chars.len(); true } else { false } } pub fn move_to_end(&mut self) -> bool { let pos = self.end(); if pos == self.pos { false } else { self.pos = pos; true } } pub fn move_to_start(&mut self) -> bool { let pos = Pos { x: 0, y: 0 }; if pos == self.pos { false } else { self.pos = pos; true } } pub fn move_to_line_end(&mut self) -> bool { let line_len = self.lines[self.pos.y].chars.len(); if self.pos.x < line_len { self.pos.x = line_len; true } else { false } } pub fn move_to_line_start(&mut self) -> bool { if self.pos.x > 0 { self.pos.x = 0; true } else { false } } pub fn move_word_left(&mut self) -> bool { if self.pos.x > 0 { let chars = &self.lines[self.pos.y].chars; loop { self.pos.x -= 1; if self.pos.x == 0 || !chars[self.pos.x-1].is_alphanumeric() { break; } } true } else { false } } pub fn move_word_right(&mut self) -> bool { if self.pos.x < self.lines[self.pos.y].chars.len() { let chars = &self.lines[self.pos.y].chars; loop { self.pos.x += 1; if self.pos.x +1 >= chars.len() || !chars[self.pos.x+1].is_alphanumeric() { break; } } true } else { false } } pub fn del_word_left(&mut self) -> bool { if self.pos.x > 0 { let chars = &mut self.lines[self.pos.y].chars; loop { self.pos.x -= 1; chars.remove(self.pos.x); if self.pos.x == 0 || !chars[self.pos.x-1].is_alphanumeric() { break; } } self.fix_selection(); true } else { false } } /// Delete the word rigth of the cursor. /// // I'm not yet sure of what should be the right behavior but all changes // should be discussed from cases defined as in the unit tests below pub fn del_word_right(&mut self) -> bool { let chars = &mut self.lines[self.pos.y].chars; if self.pos.x < chars.len() { loop { let deleted_is_an = chars[self.pos.x].is_alphanumeric(); chars.remove(self.pos.x); if !deleted_is_an { break; } if self.pos.x == chars.len() { if self.pos.x > 0 { self.pos.x -= 1; } break; } } self.fix_selection(); true } else if self.pos.x == self.current_line().chars.len() && self.pos.x > 0 { self.pos.x -= 1; true } else { false } } /// Return the number of columns taken by a char. It's /// assumed the char isn't '\r', `\n', or backspace /// (none of those can be in the inputfield lines) pub fn char_width(c: char) -> usize { match c { '\t' => TAB_REPLACEMENT.len(), _ => UnicodeWidthChar::width(c).unwrap_or(0), } } } #[test] fn test_char_iterator() { let texts = vec![ "this has\nthree lines\n", "", "123", "\n\n", ]; for text in texts { assert!(InputFieldContent::from(text).is_str(text)); } } fn is_word_char(c: char) -> bool { c.is_alphanumeric() || c == '_' } #[cfg(test)] mod input_content_edit_monoline_tests { use super::*; /// make an input for tests from two strings: /// - the content string (no wide chars) /// - a cursor position specified as a string with a caret fn make_content(value: &str, cursor_pos: &str) -> InputFieldContent { let mut content = InputFieldContent::from(value); content.pos = Pos { x: cursor_pos.chars().position(|c| c=='^').unwrap(), y: 0, }; content } fn check(a: &InputFieldContent, value: &str, cursor_pos: &str) { let b = make_content(value, cursor_pos); assert_eq!(a, &b); } /// test the behavior of new line insertion #[test] fn test_new_line() { let mut con = make_content( "12345", " ^ " ); con.insert_char('6'); check( &con, "126345", " ^ ", ); con.insert_new_line(); assert!(con.is_str("126\n345")); let mut con = InputFieldContent::default(); con.insert_char('1'); con.insert_char('2'); con.insert_new_line(); con.insert_char('3'); con.insert_char('4'); assert!(con.is_str("12\n34")); } /// test the behavior of del_word_right #[test] fn test_del_word_right() { let mut con = make_content( "aaa bbb ccc", " ^ ", ); con.del_word_right(); check( &con, "aaa bccc", " ^ ", ); con.del_word_right(); check( &con, "aaa b", " ^", ); con.del_word_right(); check( &con, "aaa ", " ^", ); con.del_word_right(); check( &con, "aaa", " ^", ); con.del_word_right(); check( &con, "aaa", " ^", ); con.del_word_right(); check( &con, "aa", " ^", ); con.del_word_right(); check( &con, "a", "^", ); con.del_word_right(); check( &con, "", "^", ); con.del_word_right(); check( &con, "", "^", ); } /// test wide_select->clear->del_selection #[test] fn test_select_clear_del_selection() { let mut con = make_content( "aaa bbb ccc", " ^ ", ); con.set_selection_tail(con.end()); con.clear(); con.del_selection(); } /// test wide_select->del_char_left->del_selection #[test] fn test_select_del_char_left_del_selection() { let mut con = make_content( "aaa bbb ccc", " ^ ", ); con.set_selection_tail(con.end()); con.del_char_left(); con.del_selection(); } } termimad-0.29.4/src/views/list_view.rs000064400000000000000000000360061046102023000160260ustar 00000000000000use { crate::{ crossterm::{ cursor::MoveTo, queue, style::{ Color, SetBackgroundColor, }, terminal::{ Clear, ClearType, }, }, compute_scrollbar, errors::Result, gray, Alignment, Area, CompoundStyle, MadSkin, Spacing, }, std::{ cmp::Ordering, io::{ stdout, Write, }, }, }; pub struct ListViewCell<'t> { con: String, style: &'t CompoundStyle, width: usize, // length of content in chars } pub struct Title { columns: Vec, // the column(s) below this title } pub struct ListViewColumn<'t, T> { title: String, min_width: usize, max_width: usize, spacing: Spacing, extract: Box ListViewCell<'t>>, // a function building cells from the rows } struct Row { data: T, displayed: bool, } /// A filterable list whose columns can be automatically resized. /// /// /// Notes: /// * another version will allow more than one style per cell /// (i.e. make the cells composites rather than compounds). Shout /// out if you need that now. /// * this version doesn't allow cell wrapping #[allow(clippy::type_complexity)] pub struct ListView<'t, T> { titles: Vec, columns: Vec<ListViewColumn<'t, T>>, rows: Vec<Row<T>>, pub area: Area, scroll: usize, pub skin: &'t MadSkin, filter: Option<Box<dyn Fn(&T) -> bool>>, // a function determining if the row must be displayed displayed_rows_count: usize, row_order: Option<Box<dyn Fn(&T, &T) -> Ordering>>, selection: Option<usize>, // index of the selected line selection_background: Color, } impl<'t> ListViewCell<'t> { pub fn new(con: String, style: &'t CompoundStyle) -> Self { let width = con.chars().count(); Self { con, style, width } } } impl<'t, T> ListViewColumn<'t, T> { pub fn new( title: &str, min_width: usize, max_width: usize, extract: Box<dyn Fn(&T) -> ListViewCell<'t>>, ) -> Self { Self { title: title.to_owned(), min_width, max_width, spacing: Spacing { width: min_width, align: Alignment::Center, }, extract, } } pub const fn with_align(mut self, align: Alignment) -> Self { self.spacing.align = align; self } } impl<'t, T> ListView<'t, T> { /// Create a new list view with the passed columns. /// /// The columns can't be changed afterwards but the area can be modified. /// When two columns have the same title, those titles are merged (but /// the columns below stay separated). pub fn new(area: Area, columns: Vec<ListViewColumn<'t, T>>, skin: &'t MadSkin) -> Self { let mut titles: Vec<Title> = Vec::new(); for (column_idx, column) in columns.iter().enumerate() { if let Some(last_title) = titles.last_mut() { if columns[last_title.columns[0]].title == column.title { // we merge those columns titles last_title.columns.push(column_idx); continue; } } // this is a new title titles.push(Title { columns: vec![column_idx], }); } Self { titles, columns, rows: Vec::new(), area, scroll: 0, skin, filter: None, displayed_rows_count: 0, row_order: None, selection: None, selection_background: gray(5), } } /// set a comparator for row sorting #[allow(clippy::type_complexity)] pub fn sort(&mut self, sort: Box<dyn Fn(&T, &T) -> Ordering>) { self.row_order = Some(sort); } /// return the height which is available for rows #[inline(always)] pub const fn tbody_height(&self) -> u16 { if self.area.height > 2 { self.area.height - 2 } else { self.area.height } } /// return an option which when filled contains /// a tupple with the top and bottom of the vertical /// scrollbar. Return none when the content fits /// the available space. #[inline(always)] pub fn scrollbar(&self) -> Option<(u16, u16)> { compute_scrollbar( self.scroll as u16, self.displayed_rows_count as u16, self.tbody_height(), self.area.top, ) } pub fn add_row(&mut self, data: T) { let stick_to_bottom = self.row_order.is_none() && self.do_scroll_show_bottom(); let displayed = match &self.filter { Some(fun) => fun(&data), None => true, }; if displayed { self.displayed_rows_count += 1; } if stick_to_bottom { self.scroll_to_bottom(); } self.rows.push(Row { data, displayed }); if let Some(row_order) = &self.row_order { self.rows.sort_by(|a, b| row_order(&a.data, &b.data)); } } /// remove all rows (and selection). /// /// Keep the columns and the sort function, if any. pub fn clear_rows(&mut self) { self.rows.clear(); self.scroll = 0; self.displayed_rows_count = 0; self.selection = None; } /// return both the number of displayed rows and the total number pub fn row_counts(&self) -> (usize, usize) { (self.displayed_rows_count, self.rows.len()) } /// recompute the widths of all columns. /// This should be called when the area size is modified pub fn update_dimensions(&mut self) { let available_width: i32 = i32::from(self.area.width) - (self.columns.len() as i32 - 1) // we remove the separator - 1; // we remove 1 to let space for the scrollbar let sum_min_widths: i32 = self.columns.iter().map(|c| c.min_width as i32).sum(); if sum_min_widths >= available_width { for i in 0..self.columns.len() { self.columns[i].spacing.width = self.columns[i].min_width; } } else { let mut excess = available_width - sum_min_widths; for i in 0..self.columns.len() { let d = ((self.columns[i].max_width - self.columns[i].min_width) as i32).min(excess); excess -= d; self.columns[i].spacing.width = self.columns[i].min_width + d as usize; } // there might be some excess, but it's better to have some space at right rather // than a too wide table } } pub fn set_filter(&mut self, filter: Box<dyn Fn(&T) -> bool>) { let mut count = 0; for row in self.rows.iter_mut() { row.displayed = filter(&row.data); if row.displayed { count += 1; } } self.scroll = 0; // something better should be done... later self.displayed_rows_count = count; self.filter = Some(filter); } pub fn remove_filter(&mut self) { for row in self.rows.iter_mut() { row.displayed = true; } self.displayed_rows_count = self.rows.len(); self.filter = None; } /// write the list view on the given writer pub fn write_on<W>(&self, w: &mut W) -> Result<()> where W: std::io::Write, { let sx = self.area.left + self.area.width; let vbar = self.skin.table.compound_style.style_char('│'); let tee = self.skin.table.compound_style.style_char('┬'); let cross = self.skin.table.compound_style.style_char('┼'); let hbar = self.skin.table.compound_style.style_char('─'); // title line queue!(w, MoveTo(self.area.left, self.area.top))?; for (title_idx, title) in self.titles.iter().enumerate() { if title_idx != 0 { vbar.queue(w)?; } let width = title .columns .iter() .map(|ci| self.columns[*ci].spacing.width) .sum::<usize>() + title.columns.len() - 1; let spacing = Spacing { width, align: Alignment::Center, }; spacing.write_str( w, &self.columns[title.columns[0]].title, &self.skin.headers[0].compound_style, )?; } // separator line queue!(w, MoveTo(self.area.left, self.area.top + 1))?; for (title_idx, title) in self.titles.iter().enumerate() { if title_idx != 0 { cross.queue(w)?; } for (col_idx_idx, col_idx) in title.columns.iter().enumerate() { if col_idx_idx > 0 { tee.queue(w)?; } for _ in 0..self.columns[*col_idx].spacing.width { hbar.queue(w)?; } } } // rows, maybe scrolled let mut row_idx = self.scroll; let scrollbar = self.scrollbar(); for y in 2..self.area.height { queue!(w, MoveTo(self.area.left, self.area.top + y))?; loop { if row_idx == self.rows.len() { queue!(w, Clear(ClearType::UntilNewLine))?; break; } if self.rows[row_idx].displayed { let selected = Some(row_idx) == self.selection; for (col_idx, col) in self.columns.iter().enumerate() { if col_idx != 0 { if selected { queue!(w, SetBackgroundColor(self.selection_background))?; } vbar.queue(w)?; } let cell = (col.extract)(&self.rows[row_idx].data); if selected { let mut style = cell.style.clone(); style.set_bg(self.selection_background); col.spacing .write_counted_str(w, &cell.con, cell.width, &style)?; } else { col.spacing .write_counted_str(w, &cell.con, cell.width, cell.style)?; } } row_idx += 1; break; } row_idx += 1; } if let Some((sctop, scbottom)) = scrollbar { queue!(w, MoveTo(sx, self.area.top + y))?; let y = y - 2; if sctop <= y && y <= scbottom { self.skin.scrollbar.thumb.queue(w)?; } else { self.skin.scrollbar.track.queue(w)?; } } } Ok(()) } /// display the whole list in its area pub fn write(&self) -> Result<()> { let mut stdout = stdout(); self.write_on(&mut stdout)?; stdout.flush()?; Ok(()) } /// return true if the last line of the list is visible pub const fn do_scroll_show_bottom(&self) -> bool { self.scroll + self.tbody_height() as usize >= self.displayed_rows_count } /// ensure the last line is visible pub fn scroll_to_bottom(&mut self) { let body_height = self.tbody_height() as usize; self.scroll = if self.displayed_rows_count > body_height { self.displayed_rows_count - body_height } else { 0 } } /// set the scroll amount. /// lines_count can be negative pub fn try_scroll_lines(&mut self, lines_count: i32) { if lines_count < 0 { let lines_count = -lines_count as usize; self.scroll = if lines_count >= self.scroll { 0 } else { self.scroll - lines_count }; } else { self.scroll = (self.scroll + lines_count as usize) .min(self.displayed_rows_count - self.tbody_height() as usize + 1); } self.make_selection_visible(); } /// set the scroll amount. /// pages_count can be negative pub fn try_scroll_pages(&mut self, pages_count: i32) { self.try_scroll_lines(pages_count * self.tbody_height() as i32) } /// try to select the next visible line pub fn try_select_next(&mut self, up: bool) { if self.displayed_rows_count == 0 { return; } if self.displayed_rows_count == 1 || self.selection.is_none() { for i in 0..self.rows.len() { let i = (i + self.scroll) % self.rows.len(); if self.rows[i].displayed { self.selection = Some(i); self.make_selection_visible(); return; } } } for i in 0..self.rows.len() { let delta_idx = if up { self.rows.len() - 1 - i } else { i + 1 }; let row_idx = (delta_idx + self.selection.unwrap()) % self.rows.len(); if self.rows[row_idx].displayed { self.selection = Some(row_idx); self.make_selection_visible(); return; } } } /// select the first visible line (unless there's nothing). pub fn select_first_line(&mut self) { for i in 0..self.rows.len() { if self.rows[i].displayed { self.selection = Some(i); self.make_selection_visible(); return; } } self.selection = None; } /// select the last visible line (unless there's nothing). pub fn select_last_line(&mut self) { for i in (0..self.rows.len()).rev() { if self.rows[i].displayed { self.selection = Some(i); self.make_selection_visible(); return; } } self.selection = None; } /// scroll to ensure the selected line (if any) is visible. /// /// This is automatically called by try_scroll /// and try select functions pub fn make_selection_visible(&mut self) { let tbody_height = self.tbody_height() as usize; if self.displayed_rows_count <= tbody_height { return; // there's no scroll } if let Some(sel) = self.selection { if sel <= self.scroll { self.scroll = if sel > 2 { sel - 2 } else { 0 }; } else if sel + 1 >= self.scroll + tbody_height { self.scroll = sel - tbody_height + 2; } } } pub fn get_selection(&self) -> Option<&T> { self.selection.map(|sel| &self.rows[sel].data) } pub const fn has_selection(&self) -> bool { self.selection.is_some() } pub fn unselect(&mut self) { self.selection = None; } } ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������termimad-0.29.4/src/views/mad_view.rs���������������������������������������������������������������0000644�0000000�0000000�00000006535�10461020230�0015620�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::{ area::Area, crossterm::event::KeyEvent, errors::Result, skin::MadSkin, views::TextView, }, crokey::KeyCombination, std::io::Write, }; /// A MadView is like a textview but it owns everything, from the /// source markdown to the area and the skin, which often makes it more convenient /// for dynamic texts. /// It's also resizeable. pub struct MadView { markdown: String, area: Area, pub skin: MadSkin, pub scroll: usize, } impl MadView { /// make a displayed text, that is a text in an area pub const fn from(markdown: String, area: Area, skin: MadSkin) -> MadView { MadView { markdown, area, skin, scroll: 0, } } /// render the markdown in the area, taking the scroll into /// account pub fn write(&self) -> Result<()> { self.write_on(&mut std::io::stdout()) } pub fn write_on<W: Write>(&self, w: &mut W) -> Result<()> { let text = self.skin.area_text(&self.markdown, &self.area); let mut text_view = TextView::from(&self.area, &text); text_view.scroll = self.scroll; text_view.write_on(w)?; Ok(()) } /// sets the new area. If it's the same as the precedent one, /// this operation does nothing. The scroll is kept if possible. pub fn resize(&mut self, area: &Area) { if *area == self.area { return; } if area.width != self.area.width { self.scroll = 0; //TODO improve } self.area.left = area.left; self.area.top = area.top; self.area.height = area.height; self.area.width = area.width; } /// set the scroll amount. /// lines_count can be negative pub fn try_scroll_lines(&mut self, lines_count: i32) { let text = self.skin.area_text(&self.markdown, &self.area); let mut text_view = TextView::from(&self.area, &text); text_view.scroll = self.scroll; text_view.try_scroll_lines(lines_count); self.scroll = text_view.scroll; } /// set the scroll amount. /// lines_count can be negative pub fn try_scroll_pages(&mut self, pages_count: i32) { self.try_scroll_lines(pages_count * i32::from(self.area.height)); } /// Apply an event being a key: page_up, page_down, up and down. /// /// Return true when the event led to a change, false when it /// was discarded. /// /// It's possible to handle the key yourself and call the try_scroll /// methods. pub fn apply_key_combination<K: Into<KeyCombination>>(&mut self, key: K) -> bool { let key = key.into(); let text = self.skin.area_text(&self.markdown, &self.area); let mut text_view = TextView::from(&self.area, &text); text_view.scroll = self.scroll; if text_view.apply_key_combination(key) { self.scroll = text_view.scroll; true } else { false } } /// Apply an event being a key: page_up, page_down, up and down. /// /// Return true when the event led to a change, false when it /// was discarded. /// /// It's possible to handle the key yourself and call the try_scroll /// methods. pub fn apply_key_event(&mut self, key: KeyEvent) -> bool { self.apply_key_combination(key) } } �������������������������������������������������������������������������������������������������������������������������������������������������������������������termimad-0.29.4/src/views/mod.rs��������������������������������������������������������������������0000644�0000000�0000000�00000000537�10461020230�0014600�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������mod input_field; mod input_field_content; mod list_view; mod mad_view; mod pos; mod progress; mod text_view; pub use { input_field::InputField, input_field_content::InputFieldContent, list_view::{ListView, ListViewCell, ListViewColumn}, mad_view::MadView, pos::{Pos, Range}, progress::ProgressBar, text_view::TextView, }; �����������������������������������������������������������������������������������������������������������������������������������������������������������������termimad-0.29.4/src/views/pos.rs��������������������������������������������������������������������0000644�0000000�0000000�00000002240�10461020230�0014613�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { std::cmp::{Ord, PartialOrd}, }; // Implementation note: the order of fields matters here // for derive implementation. /// a 2D position suitable for a cursor position /// in a text #[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Default)] pub struct Pos { pub y: usize, pub x: usize, } /// A range made of two positions, both included /// (they may be equal, in which case the range is one character long). /// /// A Range can't be empty. #[derive(Debug, Clone, Copy)] pub struct Range { pub min: Pos, pub max: Pos, } impl Pos { pub const fn new(x: usize, y: usize) -> Pos { Self { x, y } } } impl Range { pub const fn contains(&self, x: usize, y: usize) -> bool { if self.min.y == self.max.y { y == self.min.y && self.min.x <= x && x <= self.max.x } else if y < self.min.y || y > self.max.y { false } else if y == self.min.y { x >= self.min.x } else if y == self.max.y { x <= self.max.x } else { true } } pub const fn contains_pos(&self, pos: Pos) -> bool { self.contains(pos.x, pos.y) } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������termimad-0.29.4/src/views/progress.rs���������������������������������������������������������������0000644�0000000�0000000�00000001724�10461020230�0015664�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use std::fmt; // See https://en.wikipedia.org/wiki/Block_Elements for more fun static CHARS: [char; 8] = ['▏', '▎', '▍', '▌', '▋', '▊', '▉', '█']; /// A pixel precise horizontal bar pub struct ProgressBar { pub part: f32, pub chars_len: usize, } impl ProgressBar { /// create a bar of a given char length. /// `part` must be in `[0,1]`. /// `chars_len` is the max width of the bar in characters pub const fn new(part: f32, chars_len: usize) -> Self { Self { part, chars_len } } } impl fmt::Display for ProgressBar { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut s = String::new(); let mp = (self.chars_len as f32) * self.part; let full = mp as usize; for _ in 0..full { s.push(CHARS[7]); } let remain = (mp.fract() * 8.0).round() as usize; if remain > 0 { s.push(CHARS[remain - 1]); } f.pad(&s) } } ��������������������������������������������termimad-0.29.4/src/views/text_view.rs��������������������������������������������������������������0000644�0000000�0000000�00000015676�10461020230�0016051�0����������������������������������������������������������������������������������������������������ustar �����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������use { crate::{ area::Area, crossterm::{ cursor::MoveTo, event::{ KeyCode, KeyEvent, KeyModifiers, }, queue, QueueableCommand, style::Print, }, displayable_line::DisplayableLine, errors::Result, text::FmtText, SPACE_FILLING, }, crokey::{OneToThree, KeyCombination}, std::io::{stdout, Write}, }; /// A scrollable text, in a specific area. /// /// The text is assumed to have been computed for the given area. /// /// For example: /// /// ``` /// use termimad::*; /// /// // You typically borrow those 3 vars from elsewhere /// let markdown = "#title\n* item 1\n* item 2"; /// let area = Area::new(0, 0, 10, 12); /// let skin = MadSkin::default(); /// /// // displaying /// let text = skin.area_text(markdown, &area); /// let view = TextView::from(&area, &text); /// view.write().unwrap(); /// ``` /// /// This struct is just a very thin wrapper and may /// be created dynamically for renderings or event /// handling. /// /// If the text and skin are constant, you might prefer to /// use a MadView instead of a TextView: the MadView owns /// the markdown string and ensures the formatted text /// is computed accordingly to the area. pub struct TextView<'a, 't> { area: &'a Area, text: &'t FmtText<'t, 't>, pub scroll: usize, // number of lines hidden at start pub show_scrollbar: bool, } impl<'a, 't> TextView<'a, 't> { /// make a displayed text, that is a text in an area pub const fn from(area: &'a Area, text: &'t FmtText<'_, '_>) -> TextView<'a, 't> { TextView { area, text, scroll: 0, show_scrollbar: true, } } pub fn content_height(&self) -> usize { self.text.lines.len() } /// return an option which when filled contains /// a tupple with the top and bottom of the vertical /// scrollbar. Return none when the content fits /// the available space (or if show_scrollbar is false). pub fn scrollbar(&self) -> Option<(u16, u16)> { if self.show_scrollbar { self.area.scrollbar( self.scroll as u16, self.content_height() as u16, ) } else { None } } /// display the text in the area, taking the scroll into account. pub fn write(&self) -> Result<()> { let mut stdout = stdout(); self.write_on(&mut stdout)?; stdout.flush()?; Ok(()) } /// display the text in the area, taking the scroll into account. pub fn write_on<W: Write>(&self, w: &mut W) -> Result<()> { let scrollbar = self.scrollbar(); let mut lines = self.text.lines.iter().skip(self.scroll); let mut width = self.area.width as usize; if scrollbar.is_some() { width -= 1; } for j in 0..self.area.height { let y = self.area.top + j; w.queue(MoveTo(self.area.left, y))?; if let Some(line) = lines.next() { let dl = DisplayableLine::new( self.text.skin, line, Some(width), ); queue!(w, Print(&dl))?; } else { SPACE_FILLING.queue_styled(w, &self.text.skin.paragraph.compound_style, width)?; } if let Some((sctop, scbottom)) = scrollbar { if sctop <= y && y <= scbottom { self.text.skin.scrollbar.thumb.queue(w)?; } else { self.text.skin.scrollbar.track.queue(w)?; } } } Ok(()) } /// set the scroll position but makes it fit into allowed positions. /// Return the actual scroll. pub fn set_scroll(&mut self, scroll: usize) -> usize { let area_height = self.area.height as usize; self.scroll = if self.content_height() > area_height { scroll.min(self.content_height() - area_height) } else { 0 }; self.scroll } /// Change the scroll position. /// /// lines_count can be negative pub fn try_scroll_lines(&mut self, lines_count: i32) { if lines_count < 0 { let lines_count = -lines_count as usize; self.scroll = if lines_count >= self.scroll { 0 } else { self.scroll - lines_count }; } else { self.set_scroll(self.scroll + lines_count as usize); } } /// change the scroll position /// pages_count can be negative pub fn try_scroll_pages(&mut self, pages_count: i32) { self.try_scroll_lines(pages_count * i32::from(self.area.height)) } pub fn line_up(&mut self) -> bool { if self.scroll > 0 { self.scroll -= 1; true } else { false } } pub fn line_down(&mut self) -> bool { let content_height = self.content_height(); let page_height = self.area.height as usize; if self.scroll + page_height < content_height { self.scroll += 1; true } else { false } } pub fn page_up(&mut self) -> bool { let page_height = self.area.height as usize; if self.scroll > page_height { self.scroll -= page_height; true } else if self.scroll > 0 { self.scroll = 0; true } else { false } } pub fn page_down(&mut self) -> bool { let content_height = self.content_height(); let page_height = self.area.height as usize; if self.scroll + 2 * page_height < content_height { self.scroll += page_height; true } else if self.scroll + page_height < content_height { self.scroll = content_height - page_height; true } else { false } } /// Apply an event being a key: page_up, page_down, up and down. /// /// Return true when the event led to a change, false when it /// was discarded. pub fn apply_key_event(&mut self, key: KeyEvent) -> bool { self.apply_key_combination(key) } /// Apply an event being a key: page_up, page_down, up and down. /// /// Return true when the event led to a change, false when it /// was discarded. pub fn apply_key_combination<K: Into<KeyCombination>>(&mut self, key: K) -> bool { let key = key.into(); if key.modifiers != KeyModifiers::NONE { return false; } match key.codes { OneToThree::One(KeyCode::Up) => self.line_up(), OneToThree::One(KeyCode::Down) => self.line_down(), OneToThree::One(KeyCode::PageUp) => self.page_up(), OneToThree::One(KeyCode::PageDown) => self.page_down(), _ => false, } } } ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������