[
  {
    "path": ".github/workflows/linux.yml",
    "content": "name: Linux\n\non:\n  push:\n    branches: [ \"main\" ]\n  pull_request:\n    branches: [ \"main\" ]\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v4\n    - name: Install deps\n      run: |\n        sudo apt-get update\n        sudo apt-get install libpthread-stubs0-dev libgl1-mesa-dev libx11-dev libx11-xcb-dev libxcb-image0-dev libxrandr-dev libxcb-randr0-dev libudev-dev  libfreetype6-dev libglew-dev libjpeg8-dev libgpgme11-dev libjpeg62 libxcursor-dev cmake libclang-dev clang\n    - name: Build\n      run: cargo build --verbose\n    - name: Run tests\n      run: cargo test --verbose\n"
  },
  {
    "path": ".github/workflows/windows.yml",
    "content": "name: Windows\n\non:\n  push:\n    branches: [ \"main\" ]\n    tags:\n      - v*\n  pull_request:\n    branches: [ \"main\" ]\n\nenv:\n  CARGO_TERM_COLOR: always\n\njobs:\n  build:\n\n    runs-on: windows-latest\n\n    steps:\n    - uses: actions/checkout@v4\n    - name: Run tests\n      run: cargo test --verbose\n    - name: Do a release build\n      run: cargo build --release --verbose\n    - uses: actions/upload-artifact@v4\n      with:\n        name: hexerator-win64-build\n        path: target/release/hexerator.exe"
  },
  {
    "path": ".gitignore",
    "content": "/target\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\n\n## [0.4.0] - 2025-03-29\n\n### Tutorial\n\nThere is now a basic tutorial you can find here:\n<https://crumblingstatue.github.io/hexerator-book/0.4.0/tutorial/00-aitd.html>\n\n### New features\n\n- [Memory mapped file support][mmap]\n- [Allow defining data layouts with Rust struct syntax][struct]\n  - `View->Ruler` can use struct definitions\n- [Mouse drag selection][mdrag]\n  - You can finally select regions by dragging the mouse, rather than having to use shift+1/shift+2\n- [Block selection with alt+drag][mblock]\n  - You can select non-contiguous sections by holding alt and drawing a rectangle with the mouse\n\n[mmap]: <https://crumblingstatue.github.io/hexerator-book/0.4.0/feature-docs/mmap.html>\n[struct]: <https://crumblingstatue.github.io/hexerator-book/0.4.0/feature-docs/structs.html>\n[mdrag]: <https://crumblingstatue.github.io/hexerator-book/0.4.0/basic-ops/selecting-data.html#mouse-drag-selection>\n[mblock]: <https://crumblingstatue.github.io/hexerator-book/0.4.0/basic-ops/selecting-data.html#mouse-block-multi-selection>\n\n### UI changes\n\n- Add custom right panel to file open dialog\n  - Shows information about the highlighted file\n  - Allows selecting advanced options\n- Backtrace support for error popups\n- External command window now provides more options for working directory\n- Show information about rows/column positions in more places\n- `Home`/`End` now jumps to row begin/end.\n  - `ctrl+Home`/`ctrl+End` are now used for view begin/end.\n- The selection can now be quickly cleared with a `Clear` button in the top panel\n- Add a \"quick scroll\" slider popup to the bottom panel, to quickly navigate huge files.\n- Add Find&Replace for `HexString` find type\n- Add a bunch of icons to buttons\n- Remove superfluous \"Perspectives\" menu\n\n### Other Improvements\n\n- Make stream buffer size configurable, use a larger default size\n- Hexerator now retries opening a file as read-only if there was a permission error\n- Hex strings now accept parsing comma separated, or \"packed\" (unseparated) hex values\n- The command line help on Windows is now functional\n- Increase/decrease byte (`ctrl+=`/`ctrl+-`) now works on selections\n- Add Windows CI\n- Bunch of bug fixes and minor UX improvements, as usual\n\n### CLI\nAdd `--view` flag to select view to focus on startup\n\n## [0.3.0] - 2024-10-16\n\n### UI changes\n\n**Hex Editor:**\n\n- `Del` key zeroes out the byte at cursor\n\n**Bookmarks window:**\n\n- Jump-to button in detail view\n- Value edit input in detail view\n- Context menu option to copy a bookmark's offset\n- Add right click menu option to reoffset all bookmarks based on a known offset (read help label)\n\n**File diff window:**\n\n- Now takes the value types of bookmarks into account, showing the whole values of\n  bookmarks instead of just raw bytes.\n- Add \"Highlight all\" button to highlight all differences\n- Add \"Open this\" and \"Diff with...\" buttons to speed up diffing\n  subsequent versions of a file\n\n**Find dialog:**\n\n- Add help hover popups for the find type dropdown\n- Add \"string diff\" and \"pattern equivalence\" find types. See the help popups ;)\n- Add basic replace functionality to Ascii find\n\n**X86 assembly dialog:**\n\n- Add ability to jump to offset of decoded instructions\n\n**Root context menu:**\n\n- Add \"copy selection as utf-8 text\"\n- Add \"zero fill\" (Shortcut: `Del`)\n\n**External command window:**\n\n- Now openable with `Ctrl+E`\n- Allow closing with `Esc` key\n- Add \"selection only\" toggle to only pass selection to external command\n\n**Open process window:**\n- Add UI to launch a child process in order to view its memory (hexerator doesn't have to be root)\n- The virtual memory map window now makes it more clear that you're no longer\n  looking at the list of processes, but the maps for a process.\n\n**Jump dialog:**\n\n- Replace (broken) \"relative\" option with \"absolute\"\n\n**Preferences window:**\n\n- Make the ui tabbed\n- Small ui improvements\n\n### Lua scripting\n\n- Replaced LuaJIT with Lua 5.4, because LuaJIT is incompatible with `panic=abort`.\n- Add Lua syntax highlighting in most places\n- Add Lua API help window (`Scripting - Lua help`)\n- Add a bunch more API items (see `Scripting -> Lua help`)\n- Allow saving named scripts, and add script manager window to overview them\n- Add Lua console window for quick evaluation and \"watching\" expressions\n- Scripts can now take arguments (`args` table, e.g. `args.foo`)\n\n### Plugins\n\nNew feature. Allow loading dylib plugins. Documentation to be added.\nFor now, see the `hexerator_plugin_api` crate inside the repo.\n\n### Command line\n\n- Add `--version` flag\n- Add `--debug` flag to start with debug logging enabled and debug window open\n- Add `--spawn-command <command>...` flag to spawn a child process and open it in process list (hexerator doesn't have to be root)\n- Add `--autosave` and `--autoreload [<interval>]` to enable autosave/autoreaload through CLI\n- Add `--layout <name>` to switch to a layout at startup\n- Add `--new <length>` option to create a new (zero-filled) buffer\n\n### Fixes\n\n- Loading process memory on windows now correctly sets relative offset\n- When failing to load a file via command line arg, error reason is now properly displayed\n\n### Other\n\n- `Analysis -> Zero partition` for \"zero-partitioning\" files that contain large zeroed out sections (like process memory).\n- Add feature to autoreload only visible part (as opposed to whole file)\n- Replace blocking file dialog with nonblocking egui file dialog\n- Update egui to 0.29\n- Experimental support for custom color themes (See `Preferences` -> `Style`)\n- Make monochrome and \"grayscale\" hex text colors customizable\n- No more dynamic dependency on SFML. It's statically linked now.\n- Various bug fixes and minor improvements, too many to list individually\n\n## [0.2.0] - 2023-01-27\n\n### Added\n\n- Support for common value types in find dialog, in addition to u8\n- About dialog with version info + links\n- Clickable file size label in bottom right corner\n- Functionality to change the length of the data (truncate/extend)\n- Context menus in process open menu to copy addresses/sizes/etc. to clipboard\n- Right click context menu option on a view to remove it from the current layout\n- Layout properties is accessible from right click context menu on the layout\n- Error reporting message dialog if the program panics\n- Each file can set a metafile association to always load that meta when loaded\n- Vsync and fps limit settings in preferences window\n- Bookmark names are displayed when mouse hovers over a bookmarked offset\n- \"Open bookmark\" context menu option in hex view for existing bookmarks\n- \"Save as\" action\n- Hex string search in find dialog (de ad be ef)\n- Window title now includes filename of opened file\n- Ability to save/load scripts in lua execute dialog\n- `app:bookmark_set_int(name, value)` lua method to set integer value of a bookmark\n- `app:region_pattern_fill(name, pattern)` lua method to fill a region\n- Context menu to copy bookmark names in bookmarks window\n- Make the offsets in the find dialog copiable/pasteable\n- Add x86 disassembly\n\n### Changed\n\n- Update to egui 0.20\n- Open file dialog opens same directory as current file, if available\n- Replace most native message boxes with egui ones\n- Inspect panel shows value at edit cursor if mouse pointer is over a window that covers the hex view.\n- Make path label in top right corner click-to-copy\n- Process name filter in process open dialog is now case-insensitive\n- \"Diff with file\" file prompt will now open in same directory as current file\n- Don't insert a tab character for text views in edit mode when tab is pressed to switch focus\n- Active selection actions in edit menu are now in a submenu named \"Selection\"\n- \"Copy as hex\" is now known as \"Copy as hex text\"\n- Bookmarks table is now resizable horizontally\n- Bookmarks table is now scrollable vertically\n- Native dialog boxes now have a title, and their text is selectable and copyable!\n- Bookmarks window name filter is now case insensitive\n- Bookmarks window description editor is now monospace\n- Bookmark description is now in a scroll area\n- Bookmarks window \"add new at cursor\" button selects newly added bookmark automatically\n- Create default metadata for empty documents, allowing creation of binary files from scratch with Hexerator\n- File path label has context menu for various options, left clicking opens the file in default application\n\n### Fixed\n\n- Show error message box instead of panic when failing to allocate textures\n- Prevent fill dialog and Jump dialog from constantly stealing focus when they are open\n- Certain dialog types no longer erroneusly stack on top of themselves if opened multiple times.\n- Lua fill dialog with empty selection now has a close button.\n- Make regions window scroll properly\n- Pattern fill dialog is now closeable\n- \"Select all\" action now doesn't select more data than is available, even if region is bigger than data.\n\n## [0.1.0] - 2022-09-16\n\nInitial release.\n\n[0.1.0]: https://github.com/crumblingstatue/hexerator/releases/tag/v0.1.0\n[0.2.0]: https://github.com/crumblingstatue/hexerator/releases/tag/v0.2.0\n[0.3.0]: https://github.com/crumblingstatue/hexerator/releases/tag/v0.3.0\n"
  },
  {
    "path": "Cargo.toml",
    "content": "[package]\nname = \"hexerator\"\nversion = \"0.5.0-dev\"\nedition = \"2024\"\nlicense = \"MIT OR Apache-2.0\"\n\n[features]\nbackend-sfml = [\"dep:egui-sf2g\", \"dep:sf2g\"]\ndefault = [\"backend-sfml\"]\n\n[dependencies]\ngamedebug_core = { git = \"https://github.com/crumblingstatue/gamedebug_core.git\" }\nclap = { version = \"4.5.4\", features = [\"derive\"] }\nanyhow = \"1.0.81\"\nrand = \"0.10.0\"\nrmp-serde = \"1.1.2\"\nserde = { version = \"1.0.197\", features = [\"derive\"] }\ndirectories = \"6.0.0\"\nrecently_used_list = { git = \"https://github.com/crumblingstatue/recently_used_list.git\" }\nmemchr = \"2.7.2\"\nglu-sys = \"0.1.4\"\nthiserror = \"2\"\neither = \"1.10.0\"\ntree_magic_mini = \"3.1.6\"\nslotmap = { version = \"1.0.7\", features = [\"serde\"] }\negui-sf2g = { version = \"0.7\", optional = true }\nsf2g = { version = \"0.4\", optional = true, features = [\"text\"] }\nnum-traits = \"0.2.18\"\nserde-big-array = \"0.5.1\"\negui = { version = \"0.34\", features = [\"serde\"] }\negui_extras = { version = \"0.34\", default-features = false }\nitertools = \"0.14\"\nsysinfo = { version = \"0.38\", default-features = false, features = [\"system\"] }\nproc-maps = \"0.4.0\"\nopen = \"5.1.2\"\narboard = { version = \"3.6.0\", default-features = false }\npaste = \"1.0.14\"\niced-x86 = \"1.21.0\"\nstrum = { version = \"0.27\", features = [\"derive\"] }\negui_code_editor = \"0.2.14\"\n# luajit breaks with panic=abort, because it relies on unwinding for exception handling\nmlua = { version = \"0.11\", features = [\"luau\", \"vendored\"] }\negui-file-dialog.git = \"https://github.com/jannistpl/egui-file-dialog.git\"\nhuman_bytes = \"0.4.3\"\nshlex = \"1.3.0\"\negui-fontcfg = { git = \"https://github.com/crumblingstatue/egui-fontcfg.git\" }\negui_colors = \"0.11.0\"\nlibloading = \"0.9\"\nhexerator-plugin-api = { path = \"hexerator-plugin-api\" }\nimage.version = \"0.25\"\nimage.default-features = false\nimage.features = [\"png\", \"bmp\"]\nstructparse = { git = \"https://github.com/crumblingstatue/structparse.git\" }\nmemmap2 = \"0.9.5\"\negui-phosphor.git = \"https://github.com/crumblingstatue/egui-phosphor.git\"\negui-phosphor.branch = \"egui-034\"\nconstcat = \"0.6.0\"\n\n[target.\"cfg(windows)\".dependencies.windows-sys]\nversion = \"0.59.0\"\nfeatures = [\n    \"Win32_System_Diagnostics_Debug\",\n    \"Win32_Foundation\",\n    \"Win32_System_Threading\",\n]\n\n# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html\n\n# Uncomment in case incremental compilation breaks things\n#[profile.dev]\n#incremental = false # Buggy rustc is breaking code with incremental compilation\n\n# Compile deps with optimizations in dev mode\n[profile.dev.package.\"*\"]\nopt-level = 2\n\n[profile.dev]\npanic = \"abort\"\n\n[profile.release]\npanic = \"abort\"\nlto = \"thin\"\ncodegen-units = 1\n\n[build-dependencies]\nvergen-gitcl = { version = \"9.1.0\", default-features = false, features = [\n    \"build\",\n    \"cargo\",\n    \"rustc\",\n] }\n\n[workspace]\nmembers = [\"hexerator-plugin-api\", \"plugins/hello-world\"]\nexclude = [\"scripts\"]\n"
  },
  {
    "path": "LICENSE-APACHE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2022 crumblingstatue and Hexerator contributors\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "LICENSE-MIT",
    "content": "MIT License\n\nCopyright (c) 2022 crumblingstatue and Hexerator contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Hexerator\nVersatile GUI hex editor focused on binary file exploration and aiding pattern recognition. Written in Rust.\n\nCheck out [the Hexerator book](https://crumblingstatue.github.io/hexerator-book/0.4.0) for a detailed list of features, and more!\n\n## Note for contributors:\nHexerator only supports latest nightly Rust.\nYou need an up-to-date nightly to build Hexerator.\n\nHexerator doesn't shy away from experimenting with unstable Rust features.\nContributors can use any nightly feature they wish.\n\nContributors however are free to rewrite code to use stable features if it doesn't result in:\n\n- A loss of features\n- Reduced performance\n- Significantly worse maintainability\n"
  },
  {
    "path": "build.rs",
    "content": "use {\n    std::error::Error,\n    vergen_gitcl::{BuildBuilder, CargoBuilder, Emitter, GitclBuilder, RustcBuilder},\n};\n\nfn main() -> Result<(), Box<dyn Error>> {\n    let gitcl = GitclBuilder::default().sha(false).commit_timestamp(true).build()?;\n    let build = BuildBuilder::default().build_timestamp(true).build()?;\n    let cargo = CargoBuilder::default()\n        .target_triple(true)\n        .debug(true)\n        .opt_level(true)\n        .build()?;\n    let rustc = RustcBuilder::default().semver(true).build()?;\n    Emitter::default()\n        .add_instructions(&gitcl)?\n        .add_instructions(&build)?\n        .add_instructions(&cargo)?\n        .add_instructions(&rustc)?\n        .emit()?;\n    Ok(())\n}\n"
  },
  {
    "path": "hexerator-plugin-api/Cargo.toml",
    "content": "[package]\nname = \"hexerator-plugin-api\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[dependencies]"
  },
  {
    "path": "hexerator-plugin-api/src/lib.rs",
    "content": "pub trait Plugin {\n    fn name(&self) -> &str;\n    fn desc(&self) -> &str;\n    fn methods(&self) -> Vec<PluginMethod>;\n    fn on_method_called(\n        &mut self,\n        name: &str,\n        params: &[Option<Value>],\n        hx: &mut dyn HexeratorHandle,\n    ) -> MethodResult;\n}\n\npub type MethodResult = Result<Option<Value>, String>;\n\npub struct PluginMethod {\n    pub method_name: &'static str,\n    pub human_name: Option<&'static str>,\n    pub desc: &'static str,\n    pub params: &'static [MethodParam],\n}\n\npub struct MethodParam {\n    pub name: &'static str,\n    pub ty: ValueTy,\n}\n\npub enum ValueTy {\n    U64,\n    String,\n}\n\npub enum Value {\n    U64(u64),\n    F64(f64),\n    String(String),\n}\n\nimpl ValueTy {\n    pub fn label(&self) -> &'static str {\n        match self {\n            ValueTy::U64 => \"u64\",\n            ValueTy::String => \"string\",\n        }\n    }\n}\n\npub trait HexeratorHandle {\n    fn selection_range(&self) -> Option<[usize; 2]>;\n    fn get_data(&self, start: usize, end: usize) -> Option<&[u8]>;\n    fn get_data_mut(&mut self, start: usize, end: usize) -> Option<&mut [u8]>;\n    fn debug_log(&self, msg: &str);\n    fn perspective(&self, name: &str) -> Option<PerspectiveHandle>;\n    fn perspective_rows(&self, ph: &PerspectiveHandle) -> Vec<&[u8]>;\n}\n\npub struct PerspectiveHandle {\n    pub key_data: u64,\n}\n\nimpl PerspectiveHandle {\n    pub fn rows<'hx>(&self, hx: &'hx dyn HexeratorHandle) -> Vec<&'hx [u8]> {\n        hx.perspective_rows(self)\n    }\n}\n"
  },
  {
    "path": "lua/color.lua",
    "content": "return function(b)\n    local r = b\n    local g = b\n    local b = b\n    return {r % 256, g % 256, b % 256}\nend"
  },
  {
    "path": "lua/fill.lua",
    "content": "-- Return a byte based on offset `off` and the current byte value `b`\nfunction(off, b)\n    return off % 256\nend"
  },
  {
    "path": "plugins/hello-world/Cargo.toml",
    "content": "[package]\nname = \"hello-world\"\nversion = \"0.1.0\"\nedition = \"2024\"\n\n[lib]\ncrate-type = [\"cdylib\"]\n\n[dependencies]\nhexerator-plugin-api = { path = \"../../hexerator-plugin-api\" }"
  },
  {
    "path": "plugins/hello-world/src/lib.rs",
    "content": "//! Hexerator hello world example plugin\n\nuse hexerator_plugin_api::{\n    HexeratorHandle, MethodParam, MethodResult, Plugin, PluginMethod, Value, ValueTy,\n};\n\nstruct HelloPlugin;\n\nimpl Plugin for HelloPlugin {\n    fn name(&self) -> &str {\n        \"Hello world plugin\"\n    }\n\n    fn desc(&self) -> &str {\n        \"Hi! I'm an example plugin for Hexerator\"\n    }\n\n    fn methods(&self) -> Vec<hexerator_plugin_api::PluginMethod> {\n        vec![\n            PluginMethod {\n                method_name: \"say_hello\",\n                human_name: Some(\"Say hello\"),\n                desc: \"Write 'hello' to debug log.\",\n                params: &[],\n            },\n            PluginMethod {\n                method_name: \"fill_selection\",\n                human_name: Some(\"Fill selection\"),\n                desc: \"Fills the selection with 0x42\",\n                params: &[],\n            },\n            PluginMethod {\n                method_name: \"sum_range\",\n                human_name: None,\n                desc: \"Sums up the values in the provided range\",\n                params: &[\n                    MethodParam {\n                        name: \"from\",\n                        ty: ValueTy::U64,\n                    },\n                    MethodParam {\n                        name: \"to\",\n                        ty: ValueTy::U64,\n                    },\n                ],\n            },\n        ]\n    }\n\n    fn on_method_called(\n        &mut self,\n        name: &str,\n        params: &[Option<Value>],\n        hx: &mut dyn HexeratorHandle,\n    ) -> MethodResult {\n        match name {\n            \"say_hello\" => {\n                hx.debug_log(\"Hello world!\");\n                Ok(None)\n            }\n            \"fill_selection\" => match hx.selection_range() {\n                Some([start, end]) => match hx.get_data_mut(start, end) {\n                    Some(data) => {\n                        data.fill(0x42);\n                        Ok(None)\n                    }\n                    None => Err(\"Selection out of bounds\".into()),\n                },\n                None => Err(\"Selection unavailable\".into()),\n            },\n            \"sum_range\" => {\n                let &[Some(Value::U64(from)), Some(Value::U64(to))] = params else {\n                    return Err(\"Invalid params\".into());\n                };\n                match hx.get_data_mut(from as usize, to as usize) {\n                    Some(data) => {\n                        let sum: u64 = data.iter().map(|b| *b as u64).sum();\n                        Ok(Some(Value::U64(sum)))\n                    }\n                    None => Err(\"Out of bounds\".into()),\n                }\n            }\n            _ => Err(format!(\"Unknown method: {name}\")),\n        }\n    }\n}\n\n#[unsafe(no_mangle)]\npub extern \"Rust\" fn hexerator_plugin_new() -> Box<dyn Plugin> {\n    Box::new(HelloPlugin)\n}\n"
  },
  {
    "path": "rust-toolchain.toml",
    "content": "[toolchain]\nchannel = \"nightly\"\n# Lock to a specific nightly before release\n#channel = \"nightly-2025-03-22\"\n"
  },
  {
    "path": "rustfmt.toml",
    "content": "imports_granularity = \"One\"\ngroup_imports = \"One\"\nchain_width = 80"
  },
  {
    "path": "scripts/gen-prim-test-file.rs",
    "content": "#!/usr/bin/env -S cargo +nightly -Zscript\n\nuse std::{fs::File, io::Write};\n\nfn main() {\n    let mut f = File::create(\"test_files/primitives.bin\").unwrap();\n    macro_rules! prim {\n        ($t:ident, $val:literal) => {\n            let v: $t = $val;\n            let mut buf = std::io::Cursor::new([0u8; 48]);\n            // Write desc\n            write!(&mut buf, \"{} = {}\", stringify!($t), v).unwrap();\n            f.write_all(buf.get_ref()).unwrap();\n            // Write byte repr\n            // le\n            buf.get_mut().fill(0);\n            buf.get_mut()[..std::mem::size_of::<$t>()].copy_from_slice(&v.to_le_bytes());\n            f.write_all(buf.get_ref()).unwrap();\n            // be\n            buf.get_mut().fill(0);\n            buf.get_mut()[..std::mem::size_of::<$t>()].copy_from_slice(&v.to_be_bytes());\n            f.write_all(buf.get_ref()).unwrap();\n        };\n    }\n    prim!(u8, 42);\n    prim!(i8, 42);\n    prim!(u16, 4242);\n    prim!(i16, 4242);\n    prim!(u32, 424242);\n    prim!(i32, 424242);\n    prim!(u64, 424242424242);\n    prim!(i64, 424242424242);\n    prim!(u128, 424242424242424242424242);\n    prim!(i128, 424242424242424242424242);\n    prim!(f32, 42.4242);\n    prim!(f64, 4242.42424242);\n}\n"
  },
  {
    "path": "src/app/backend_command.rs",
    "content": "//! This module is similar in purpose to [`crate::app::command`].\n//!\n//! See that module for more information.\n\nuse {\n    super::App, crate::config::Config, egui_sf2g::sf2g::graphics::RenderWindow,\n    std::collections::VecDeque,\n};\n\npub enum BackendCmd {\n    SetWindowTitle(String),\n    ApplyVsyncCfg,\n    ApplyFpsLimit,\n}\n\n/// Gui command queue.\n///\n/// Push operations with `push`, and call [`App::flush_backend_command_queue`] when you have\n/// exclusive access to the [`App`].\n///\n/// [`App::flush_backend_command_queue`] is called automatically every frame, if you don't need to perform the operations sooner.\n#[derive(Default)]\npub struct BackendCommandQueue {\n    inner: VecDeque<BackendCmd>,\n}\n\nimpl BackendCommandQueue {\n    pub fn push(&mut self, command: BackendCmd) {\n        self.inner.push_back(command);\n    }\n}\n\nimpl App {\n    /// Flush the [`BackendCommandQueue`] and perform all operations queued up.\n    ///\n    /// Automatically called every frame, but can be called manually if operations need to be\n    /// performed sooner.\n    pub fn flush_backend_command_queue(&mut self, rw: &mut RenderWindow) {\n        while let Some(cmd) = self.backend_cmd.inner.pop_front() {\n            perform_command(cmd, rw, &self.cfg);\n        }\n    }\n}\n\nfn perform_command(cmd: BackendCmd, rw: &mut RenderWindow, cfg: &Config) {\n    match cmd {\n        BackendCmd::SetWindowTitle(title) => rw.set_title(&title),\n        BackendCmd::ApplyVsyncCfg => {\n            rw.set_vertical_sync_enabled(cfg.vsync);\n        }\n        BackendCmd::ApplyFpsLimit => {\n            rw.set_framerate_limit(cfg.fps_limit);\n        }\n    }\n}\n"
  },
  {
    "path": "src/app/command.rs",
    "content": "//! Due to various issues with overlapping borrows, it's not always feasible to do every operation\n//! on the application state at the time the action is requested.\n//!\n//! Sometimes we need to wait until we have exclusive access to the application before we can\n//! perform an operation.\n//!\n//! One possible way to do this is to encode whatever data an operation requires, and save it until\n//! we have exclusive access, and then perform it.\n\nuse {\n    super::{App, backend_command::BackendCmd},\n    crate::{\n        damage_region::DamageRegion,\n        data::Data,\n        gui::Gui,\n        meta::{NamedView, PerspectiveKey, RegionKey},\n        scripting::exec_lua,\n        shell::msg_if_fail,\n        view::{HexData, View, ViewKind},\n    },\n    mlua::Lua,\n    std::{collections::VecDeque, path::Path},\n};\n\npub enum Cmd {\n    CreatePerspective {\n        region_key: RegionKey,\n        name: String,\n    },\n    RemovePerspective(PerspectiveKey),\n    SetSelection(usize, usize),\n    SetAndFocusCursor(usize),\n    SetLayout(crate::meta::LayoutKey),\n    FocusView(crate::meta::ViewKey),\n    CreateView {\n        perspective_key: PerspectiveKey,\n        name: String,\n    },\n    /// Finish saving a truncated file\n    SaveTruncateFinish,\n    /// Extend (or truncate) the data buffer to a new length\n    ExtendDocument {\n        new_len: usize,\n    },\n    /// Paste bytes at the requested index\n    PasteBytes {\n        at: usize,\n        bytes: Vec<u8>,\n    },\n    /// A new source was loaded, process the changes\n    ProcessSourceChange,\n}\n\n/// Application command queue.\n///\n/// Push operations with `push`, and call `App::flush_command_queue` when you have\n/// exclusive access to the `App`.\n///\n/// `App::flush_command_queue` is called automatically every frame, if you don't need to perform the operations sooner.\n#[derive(Default)]\npub struct CommandQueue {\n    inner: VecDeque<Cmd>,\n}\n\nimpl CommandQueue {\n    pub fn push(&mut self, command: Cmd) {\n        self.inner.push_back(command);\n    }\n}\n\nimpl App {\n    /// Flush the [`CommandQueue`] and perform all operations queued up.\n    ///\n    /// Automatically called every frame, but can be called manually if operations need to be\n    /// performed sooner.\n    pub fn flush_command_queue(\n        &mut self,\n        gui: &mut Gui,\n        lua: &Lua,\n        font_size: u16,\n        line_spacing: u16,\n    ) {\n        while let Some(cmd) = self.cmd.inner.pop_front() {\n            perform_command(self, cmd, gui, lua, font_size, line_spacing);\n        }\n    }\n}\n\n/// Perform a command. Called by `App::flush_command_queue`, but can be called manually if you\n/// have a `Cmd` you would like you perform.\npub fn perform_command(\n    app: &mut App,\n    cmd: Cmd,\n    gui: &mut Gui,\n    lua: &Lua,\n    font_size: u16,\n    line_spacing: u16,\n) {\n    match cmd {\n        Cmd::CreatePerspective { region_key, name } => {\n            let per_key = app.add_perspective_from_region(region_key, name);\n            gui.win.perspectives.open.set(true);\n            gui.win.perspectives.rename_idx = per_key;\n        }\n        Cmd::SetSelection(a, b) => {\n            app.hex_ui.select_a = Some(a);\n            app.hex_ui.select_b = Some(b);\n        }\n        Cmd::SetAndFocusCursor(off) => {\n            app.edit_state.cursor = off;\n            app.center_view_on_offset(off);\n            app.hex_ui.flash_cursor();\n        }\n        Cmd::SetLayout(key) => app.hex_ui.current_layout = key,\n        Cmd::FocusView(key) => app.hex_ui.focused_view = Some(key),\n        Cmd::RemovePerspective(key) => {\n            app.meta_state.meta.low.perspectives.remove(key);\n            // TODO: Should probably handle dangling keys somehow.\n            // either by not allowing removal in that case, or being robust against dangling keys\n            // or removing everything that uses a dangling key.\n        }\n        Cmd::CreateView {\n            perspective_key,\n            name,\n        } => {\n            app.meta_state.meta.views.insert(NamedView {\n                view: View::new(\n                    ViewKind::Hex(HexData::with_font_size(font_size)),\n                    perspective_key,\n                ),\n                name,\n            });\n        }\n        Cmd::SaveTruncateFinish => {\n            msg_if_fail(\n                app.save_truncated_file_finish(),\n                \"Save error\",\n                &mut gui.msg_dialog,\n            );\n        }\n        Cmd::ExtendDocument { new_len } => {\n            app.data.resize(new_len, 0);\n        }\n        Cmd::PasteBytes { at, bytes } => {\n            let range = at..at + bytes.len();\n            app.data[range.clone()].copy_from_slice(&bytes);\n            app.data.widen_dirty_region(DamageRegion::Range(range));\n        }\n        Cmd::ProcessSourceChange => {\n            // Allocate a clean data buffer for streaming sources\n            if app.source.as_ref().is_some_and(|src| src.attr.stream) {\n                app.data = Data::clean_from_buf(Vec::new());\n            }\n            app.backend_cmd.push(BackendCmd::SetWindowTitle(format!(\n                \"{} - Hexerator\",\n                app.source_file().map_or(\"no source\", path_filename_as_str)\n            )));\n            if let Some(key) = &app.meta_state.meta.onload_script {\n                let scr = &app.meta_state.meta.scripts[*key];\n                let content = scr.content.clone();\n                let result = exec_lua(\n                    lua,\n                    &content,\n                    app,\n                    gui,\n                    \"\",\n                    Some(*key),\n                    font_size,\n                    line_spacing,\n                );\n                msg_if_fail(\n                    result,\n                    \"Failed to execute onload lua script\",\n                    &mut gui.msg_dialog,\n                );\n            }\n        }\n    }\n}\n\nfn path_filename_as_str(path: &Path) -> &str {\n    path.file_name()\n        .map_or(\"<no_filename>\", |osstr| osstr.to_str().unwrap_or_default())\n}\n"
  },
  {
    "path": "src/app/debug.rs",
    "content": "#![allow(unused_imports)]\nuse {\n    super::App,\n    gamedebug_core::{imm, imm_dbg},\n};\n\nimpl App {\n    /// Central place to put some immediate state debugging (using gamedebug_core)\n    pub(crate) fn imm_debug_fun(&self) {\n        // Put immediate debugging code here (F12 to open debug console)\n    }\n}\n"
  },
  {
    "path": "src/app/edit_state.rs",
    "content": "#[derive(Default, Debug)]\npub struct EditState {\n    // The editing byte offset\n    pub cursor: usize,\n    cursor_history: Vec<usize>,\n    cursor_history_current: usize,\n}\n\nimpl EditState {\n    /// Set cursor and save history\n    pub fn set_cursor(&mut self, offset: usize) {\n        self.cursor_history.truncate(self.cursor_history_current);\n        self.cursor_history.push(self.cursor);\n        self.cursor = offset;\n        self.cursor_history_current += 1;\n    }\n    /// Set cursor, don't save history\n    pub fn set_cursor_no_history(&mut self, offset: usize) {\n        self.cursor = offset;\n    }\n    /// Step cursor forward without saving history\n    pub fn step_cursor_forward(&mut self) {\n        self.cursor += 1;\n    }\n    /// Step cursor back without saving history\n    pub fn step_cursor_back(&mut self) {\n        self.cursor = self.cursor.saturating_sub(1);\n    }\n    /// Offset cursor by amount, not saving history\n    pub fn offset_cursor(&mut self, amount: usize) {\n        self.cursor += amount;\n    }\n    pub fn cursor_history_back(&mut self) -> bool {\n        if self.cursor_history_current > 0 {\n            self.cursor_history.push(self.cursor);\n            self.cursor_history_current -= 1;\n            self.cursor = self.cursor_history[self.cursor_history_current];\n            true\n        } else {\n            false\n        }\n    }\n    pub fn cursor_history_forward(&mut self) -> bool {\n        if self.cursor_history_current + 1 < self.cursor_history.len() {\n            self.cursor_history_current += 1;\n            self.cursor = self.cursor_history[self.cursor_history_current];\n            true\n        } else {\n            false\n        }\n    }\n}\n"
  },
  {
    "path": "src/app/interact_mode.rs",
    "content": "/// User interaction mode\n///\n/// There are 2 modes: View and Edit\n#[derive(PartialEq, Eq, Debug)]\npub enum InteractMode {\n    /// Mode optimized for viewing the contents\n    ///\n    /// For example arrow keys scroll the content\n    View,\n    /// Mode optimized for editing the contents\n    ///\n    /// For example arrow keys move the cursor\n    Edit,\n}\n"
  },
  {
    "path": "src/app/presentation.rs",
    "content": "use {\n    crate::{\n        color::{RgbaColor, rgba},\n        value_color::ColorMethod,\n    },\n    serde::{Deserialize, Serialize},\n};\n\n#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]\npub struct Presentation {\n    pub color_method: ColorMethod,\n    pub invert_color: bool,\n    pub sel_color: RgbaColor,\n    pub cursor_color: RgbaColor,\n    pub cursor_active_color: RgbaColor,\n}\n\nimpl Default for Presentation {\n    fn default() -> Self {\n        Self {\n            color_method: ColorMethod::Default,\n            invert_color: false,\n            sel_color: rgba(75, 75, 75, 255),\n            cursor_color: rgba(160, 160, 160, 255),\n            cursor_active_color: rgba(255, 255, 255, 255),\n        }\n    }\n}\n"
  },
  {
    "path": "src/app.rs",
    "content": "use {\n    self::{\n        backend_command::BackendCommandQueue,\n        command::{Cmd, CommandQueue},\n        edit_state::EditState,\n    },\n    crate::{\n        args::{Args, SourceArgs},\n        config::Config,\n        damage_region::DamageRegion,\n        data::Data,\n        gui::{\n            Gui,\n            message_dialog::{Icon, MessageDialog},\n            windows::FileDiffResultWindow,\n        },\n        hex_ui::HexUi,\n        input::Input,\n        layout::{Layout, default_margin, do_auto_layout},\n        meta::{\n            LayoutKey, Meta, NamedRegion, NamedView, PerspectiveKey, PerspectiveMap, RegionKey,\n            RegionMap, ViewKey, perspective::Perspective, region::Region,\n        },\n        meta_state::MetaState,\n        plugin::PluginContainer,\n        result_ext::AnyhowConv as _,\n        session_prefs::{Autoreload, SessionPrefs},\n        shell::{msg_fail, msg_if_fail},\n        source::{Source, SourceAttributes, SourcePermissions, SourceProvider, SourceState},\n        view::{HexData, TextData, View, ViewKind, ViewportScalar},\n    },\n    anyhow::Context as _,\n    egui_sf2g::sf2g::graphics::RenderWindow,\n    gamedebug_core::{per, per_dbg},\n    hexerator_plugin_api::MethodResult,\n    mlua::Lua,\n    slotmap::Key as _,\n    std::{\n        ffi::OsString,\n        fs::{File, OpenOptions},\n        io::{Read as _, Seek as _, SeekFrom, Write as _},\n        path::{Path, PathBuf},\n        sync::mpsc::Receiver,\n        thread,\n        time::Instant,\n    },\n};\n\npub mod backend_command;\npub mod command;\nmod debug;\npub mod edit_state;\npub mod interact_mode;\npub mod presentation;\n\n/// The hexerator application state\npub struct App {\n    pub data: Data,\n    pub edit_state: EditState,\n    pub input: Input,\n    pub src_args: SourceArgs,\n    pub source: Option<Source>,\n    stream_read_recv: Option<Receiver<Vec<u8>>>,\n    pub cfg: Config,\n    last_reload: Instant,\n    pub preferences: SessionPrefs,\n    pub hex_ui: HexUi,\n    pub meta_state: MetaState,\n    pub clipboard: arboard::Clipboard,\n    /// Command queue for queuing up operations to perform on the application state.\n    pub cmd: CommandQueue,\n    pub backend_cmd: BackendCommandQueue,\n    /// A quit was requested\n    pub quit_requested: bool,\n    pub plugins: Vec<PluginContainer>,\n    /// Size of the buffer used for streaming reads\n    pub stream_buffer_size: usize,\n}\n\nconst DEFAULT_STREAM_BUFFER_SIZE: usize = 65_536;\n\n/// Source management\nimpl App {\n    pub fn reload(&mut self) -> anyhow::Result<()> {\n        match &mut self.source {\n            Some(src) => match &mut src.provider {\n                SourceProvider::File(file) => {\n                    self.data.reload_from_file(&self.src_args, file)?;\n                }\n                SourceProvider::Stdin(_) => {\n                    anyhow::bail!(\"Can't reload streaming sources like standard input\")\n                }\n                #[cfg(windows)]\n                SourceProvider::WinProc {\n                    handle,\n                    start,\n                    size,\n                } => unsafe {\n                    crate::windows::read_proc_memory(*handle, &mut self.data, *start, *size)?;\n                },\n            },\n            None => anyhow::bail!(\"No file to reload\"),\n        }\n        Ok(())\n    }\n    pub(crate) fn load_file_args(\n        &mut self,\n        mut src_args: SourceArgs,\n        meta_path: Option<PathBuf>,\n        msg: &mut MessageDialog,\n        font_size: u16,\n        line_spacing: u16,\n        column_count: Option<usize>,\n    ) {\n        if load_file_from_src_args(\n            &mut src_args,\n            &mut self.cfg,\n            &mut self.source,\n            &mut self.data,\n            msg,\n            &mut self.cmd,\n        ) {\n            // Set up meta\n            if !self.preferences.keep_meta {\n                if let Some(meta_path) = meta_path {\n                    if let Err(e) = self.consume_meta_from_file(meta_path, false) {\n                        self.set_new_clean_meta(font_size, line_spacing, column_count);\n                        msg_fail(&e, \"Failed to load metafile\", msg);\n                    }\n                } else if let Some(src_path) = per_dbg!(&src_args.file)\n                    && let Some(meta_path) = per_dbg!(self.cfg.meta_assocs.get(src_path))\n                {\n                    // We only load if the new meta path is not the same as the old.\n                    // Keep the current metafile otherwise\n                    if self.meta_state.current_meta_path != *meta_path {\n                        per!(\n                            \"Mismatch: {:?} vs. {:?}\",\n                            self.meta_state.current_meta_path.display(),\n                            meta_path.display()\n                        );\n                        let meta_path = meta_path.clone();\n                        if let Err(e) = self.consume_meta_from_file(meta_path.clone(), false) {\n                            self.set_new_clean_meta(font_size, line_spacing, column_count);\n                            msg_fail(&e, &format!(\"Failed to load metafile {meta_path:?}\"), msg);\n                        }\n                    }\n                } else {\n                    // We didn't load any meta, but we're loading a new file.\n                    // Set up a new clean meta for it.\n                    self.set_new_clean_meta(font_size, line_spacing, column_count);\n                }\n            }\n            self.src_args = src_args;\n            if let Some(offset) = self.src_args.jump {\n                self.center_view_on_offset(offset);\n                self.edit_state.cursor = offset;\n                self.hex_ui.flash_cursor();\n            }\n        }\n    }\n    pub fn save(&mut self, msg: &mut MessageDialog) -> anyhow::Result<()> {\n        let file = match &mut self.source {\n            Some(src) => match &mut src.provider {\n                SourceProvider::File(file) => file,\n                SourceProvider::Stdin(_) => anyhow::bail!(\"Standard input doesn't support saving\"),\n                #[cfg(windows)]\n                SourceProvider::WinProc { handle, start, .. } => {\n                    if let Some(region) = self.data.dirty_region {\n                        let mut n_write = 0;\n                        unsafe {\n                            if windows_sys::Win32::System::Diagnostics::Debug::WriteProcessMemory(\n                                *handle,\n                                (*start + region.begin) as _,\n                                self.data[region.begin..].as_mut_ptr() as _,\n                                region.len(),\n                                &mut n_write,\n                            ) == 0\n                            {\n                                anyhow::bail!(\"Failed to write process memory\");\n                            }\n                        }\n                        self.data.dirty_region = None;\n                    }\n                    return Ok(());\n                }\n            },\n            None => anyhow::bail!(\"No surce opened, nothing to save\"),\n        };\n        // If the file was truncated, we completely save over it\n        if self.data.len() != self.data.orig_data_len {\n            msg.open(\n                Icon::Warn,\n                \"File truncated/extended\",\n                \"Data is truncated/extended. Are you sure you want to save?\",\n            );\n            msg.custom_button_row_ui(Box::new(|ui, payload, cmd| {\n                if ui\n                    .button(egui::RichText::new(\"Save & Truncate\").color(egui::Color32::RED))\n                    .clicked()\n                {\n                    payload.close = true;\n                    cmd.push(Cmd::SaveTruncateFinish);\n                }\n                if ui.button(\"Cancel\").clicked() {\n                    payload.close = true;\n                }\n            }));\n            return Ok(());\n        }\n        let offset = self.src_args.hard_seek.unwrap_or(0);\n        file.seek(SeekFrom::Start(offset as u64))?;\n        let data_to_write = match self.data.dirty_region {\n            Some(region) => {\n                #[expect(\n                    clippy::cast_possible_wrap,\n                    reason = \"Files bigger than i64::MAX aren't supported\"\n                )]\n                file.seek(SeekFrom::Current(region.begin as _))?;\n                // TODO: We're assuming here that end of the region is the same position as the last dirty byte\n                // Make sure to enforce this invariant.\n                // Add 1 to the end to write the dirty region even if it's 1 byte\n                self.data.get(region.begin..region.end + 1)\n            }\n            None => Some(&self.data[..]),\n        };\n        let Some(data_to_write) = data_to_write else {\n            anyhow::bail!(\"No data to write (possibly out of bounds indexing)\");\n        };\n        file.write_all(data_to_write)?;\n        self.data.undirty();\n        if let Err(e) = self.save_temp_metafile_backup() {\n            per!(\"Failed to save metafile backup: {}\", e);\n        }\n        Ok(())\n    }\n    pub fn save_truncated_file_finish(&mut self) -> anyhow::Result<()> {\n        let Some(source) = &mut self.source else {\n            anyhow::bail!(\"There is no source\");\n        };\n        let SourceProvider::File(file) = &mut source.provider else {\n            anyhow::bail!(\"Source is not a file\");\n        };\n        file.set_len(self.data.len() as u64)?;\n        file.rewind()?;\n        file.write_all(&self.data)?;\n        self.data.undirty();\n        Ok(())\n    }\n    pub(crate) fn source_file(&self) -> Option<&Path> {\n        self.src_args.file.as_deref()\n    }\n    pub(crate) fn load_file(\n        &mut self,\n        path: PathBuf,\n        read_only: bool,\n        msg: &mut MessageDialog,\n        font_size: u16,\n        line_spacing: u16,\n    ) {\n        self.load_file_args(\n            SourceArgs {\n                file: Some(path),\n                jump: None,\n                hard_seek: None,\n                take: None,\n                read_only,\n                stream: false,\n                stream_buffer_size: None,\n                unsafe_mmap: None,\n                mmap_len: None,\n            },\n            None,\n            msg,\n            font_size,\n            line_spacing,\n            None,\n        );\n    }\n\n    pub fn close_file(&mut self) {\n        // We potentially had large data, free it instead of clearing the Vec\n        self.data.close();\n        self.src_args.file = None;\n        self.source = None;\n    }\n\n    pub(crate) fn backup_path(&self) -> Option<PathBuf> {\n        self.src_args.file.as_ref().map(|file| {\n            let mut os_string = OsString::from(file);\n            os_string.push(\".hexerator_bak\");\n            os_string.into()\n        })\n    }\n\n    pub(crate) fn restore_backup(&mut self) -> Result<(), anyhow::Error> {\n        std::fs::copy(\n            self.backup_path().context(\"Failed to get backup path\")?,\n            self.src_args.file.as_ref().context(\"No file open\")?,\n        )?;\n        self.reload()\n    }\n\n    pub(crate) fn create_backup(&self) -> Result<(), anyhow::Error> {\n        std::fs::copy(\n            self.src_args.file.as_ref().context(\"No file open\")?,\n            self.backup_path().context(\"Failed to get backup path\")?,\n        )?;\n        Ok(())\n    }\n    /// Reload only what's visible on the screen (current layout)\n    fn reload_visible(&mut self) -> anyhow::Result<()> {\n        let [lo, hi] = self.visible_byte_range();\n        self.reload_range(lo, hi)\n    }\n    pub fn reload_range(&mut self, lo: usize, hi: usize) -> anyhow::Result<()> {\n        let Some(src) = &self.source else {\n            anyhow::bail!(\"No source\")\n        };\n        anyhow::ensure!(lo <= hi);\n        match &src.provider {\n            SourceProvider::File(file) => {\n                let mut file = file;\n                let offset = match self.src_args.hard_seek {\n                    Some(hs) => hs + lo,\n                    None => lo,\n                };\n                file.seek(SeekFrom::Start(offset as u64))?;\n                match self.data.get_mut(lo..=hi) {\n                    Some(buf) => file.read_exact(buf)?,\n                    None => anyhow::bail!(\"Reload range out of bounds\"),\n                }\n                Ok(())\n            }\n            SourceProvider::Stdin(_) => anyhow::bail!(\"Not implemented\"),\n            #[cfg(windows)]\n            SourceProvider::WinProc { .. } => anyhow::bail!(\"Not implemented\"),\n        }\n    }\n    #[allow(clippy::unnecessary_wraps, reason = \"cfg shenanigans\")]\n    pub(crate) fn load_proc_memory(\n        &mut self,\n        pid: sysinfo::Pid,\n        start: usize,\n        size: usize,\n        is_write: bool,\n        msg: &mut MessageDialog,\n        font_size: u16,\n        line_spacing: u16,\n    ) -> anyhow::Result<()> {\n        #[cfg(target_os = \"linux\")]\n        {\n            load_proc_memory_linux(\n                self,\n                pid,\n                start,\n                size,\n                is_write,\n                msg,\n                font_size,\n                line_spacing,\n            );\n            Ok(())\n        }\n        #[cfg(windows)]\n        return crate::windows::load_proc_memory(\n            self,\n            pid,\n            start,\n            size,\n            is_write,\n            font_size,\n            line_spacing,\n            msg,\n        );\n        #[cfg(target_os = \"macos\")]\n        return load_proc_memory_macos(self, pid, start, size, is_write, font, msg);\n    }\n}\n\nconst DEFAULT_COLUMN_COUNT: usize = 48;\n\n/// Metafile\nimpl App {\n    /// Set a new clean meta for the current data, and switch to default layout\n    pub fn set_new_clean_meta(\n        &mut self,\n        font_size: u16,\n        line_spacing: u16,\n        column_count: Option<usize>,\n    ) {\n        per!(\"Setting up new clean meta\");\n        self.meta_state.current_meta_path.clear();\n        self.meta_state.meta = Meta::default();\n        let layout_key = setup_empty_meta(\n            self.data.len(),\n            &mut self.meta_state.meta,\n            font_size,\n            line_spacing,\n            column_count.unwrap_or(DEFAULT_COLUMN_COUNT),\n        );\n        self.meta_state.clean_meta = self.meta_state.meta.clone();\n        Self::switch_layout(&mut self.hex_ui, &self.meta_state.meta, layout_key);\n    }\n    /// Like `set_new_clean_meta`, but keeps the clean meta intact\n    ///\n    /// Used for \"Clear meta\" action.\n    pub fn clear_meta(&mut self, font_size: u16, line_spacing: u16) {\n        self.meta_state.meta = Meta::default();\n        let layout_key = setup_empty_meta(\n            self.data.len(),\n            &mut self.meta_state.meta,\n            font_size,\n            line_spacing,\n            DEFAULT_COLUMN_COUNT,\n        );\n        Self::switch_layout(&mut self.hex_ui, &self.meta_state.meta, layout_key);\n    }\n    pub fn save_temp_metafile_backup(&mut self) -> anyhow::Result<()> {\n        // We set the last_meta_backup first, so if save fails, we don't get\n        // a never ending stream of constant save failures.\n        self.meta_state.last_meta_backup.set(Instant::now());\n        self.save_meta_to_file(temp_metafile_backup_path(), true)?;\n        per!(\"Saved temp metafile backup\");\n        Ok(())\n    }\n    pub fn save_meta_to_file(&mut self, path: PathBuf, temp: bool) -> Result<(), anyhow::Error> {\n        let data = rmp_serde::to_vec(&self.meta_state.meta)?;\n        std::fs::write(&path, data)?;\n        if !temp {\n            self.meta_state.current_meta_path = path;\n            self.meta_state.clean_meta = self.meta_state.meta.clone();\n        }\n        Ok(())\n    }\n    pub fn save_meta(&mut self) -> Result<(), anyhow::Error> {\n        self.save_meta_to_file(self.meta_state.current_meta_path.clone(), false)\n    }\n    pub fn consume_meta_from_file(\n        &mut self,\n        path: PathBuf,\n        temp: bool,\n    ) -> Result<(), anyhow::Error> {\n        per!(\"Consuming metafile: {}\", path.display());\n        let data = std::fs::read(&path)?;\n        let meta = rmp_serde::from_slice(&data).context(\"Deserialization error\")?;\n        self.hex_ui.clear_meta_refs();\n        self.meta_state.meta = meta;\n        if !temp {\n            self.meta_state.current_meta_path = path;\n            self.meta_state.clean_meta = self.meta_state.meta.clone();\n        }\n        self.meta_state.meta.post_load_init();\n        // Switch to first layout, if there is one\n        if let Some(layout_key) = self.meta_state.meta.layouts.keys().next() {\n            Self::switch_layout(&mut self.hex_ui, &self.meta_state.meta, layout_key);\n        }\n        Ok(())\n    }\n\n    pub fn add_perspective_from_region(\n        &mut self,\n        region_key: RegionKey,\n        name: String,\n    ) -> PerspectiveKey {\n        let mut per = Perspective::from_region(region_key, name);\n        if let Some(focused_per) = Self::focused_perspective(&self.hex_ui, &self.meta_state.meta) {\n            per.cols = focused_per.cols;\n        }\n        self.meta_state.meta.low.perspectives.insert(per)\n    }\n}\n\n/// Navigation\nimpl App {\n    pub fn search_focus(&mut self, offset: usize) {\n        self.edit_state.cursor = offset;\n        self.center_view_on_offset(offset);\n        self.hex_ui.flash_cursor();\n    }\n    pub(crate) fn center_view_on_offset(&mut self, offset: usize) {\n        if let Some(key) = self.hex_ui.focused_view {\n            self.meta_state.meta.views[key].view.center_on_offset(\n                offset,\n                &self.meta_state.meta.low.perspectives,\n                &self.meta_state.meta.low.regions,\n            );\n        }\n    }\n    pub fn cursor_history_back(&mut self) {\n        if self.edit_state.cursor_history_back() {\n            self.center_view_on_offset(self.edit_state.cursor);\n            self.hex_ui.flash_cursor();\n        }\n    }\n    pub fn cursor_history_forward(&mut self) {\n        if self.edit_state.cursor_history_forward() {\n            self.center_view_on_offset(self.edit_state.cursor);\n            self.hex_ui.flash_cursor();\n        }\n    }\n\n    pub(crate) fn set_cursor_init(&mut self) {\n        self.edit_state.cursor = self.src_args.jump.unwrap_or(0);\n        self.center_view_on_offset(self.edit_state.cursor);\n        self.hex_ui.flash_cursor();\n    }\n    pub(crate) fn switch_layout(app_hex_ui: &mut HexUi, app_meta: &Meta, k: LayoutKey) {\n        app_hex_ui.current_layout = k;\n        // Set focused view to the first available view in the layout\n        if let Some(view_key) = app_meta.layouts[k].view_grid.first().and_then(|row| row.first()) {\n            app_hex_ui.focused_view = Some(*view_key);\n        }\n    }\n    /// Tries to switch to a layout with the given name. Returns `false` if a layout with that name wasn't found.\n    #[must_use]\n    pub(crate) fn switch_layout_by_name(\n        app_hex_ui: &mut HexUi,\n        app_meta: &Meta,\n        name: &str,\n    ) -> bool {\n        match app_meta.layouts.iter().find(|(_k, v)| v.name == name) {\n            Some((k, _v)) => {\n                Self::switch_layout(app_hex_ui, app_meta, k);\n                true\n            }\n            None => false,\n        }\n    }\n\n    /// Tries to focus a view with the given name. Returns `false` if a view with that name wasn't found.\n    #[must_use]\n    pub(crate) fn focus_first_view_of_name(\n        app_hex_ui: &mut HexUi,\n        app_meta: &Meta,\n        name: &str,\n    ) -> bool {\n        match app_meta.views.iter().find(|(_k, v)| v.name == name) {\n            Some((k, _v)) => {\n                Self::focus_first_view_of_key(app_hex_ui, app_meta, k);\n                true\n            }\n            None => false,\n        }\n    }\n\n    pub(crate) fn focus_prev_view_in_layout(&mut self) {\n        if let Some(focused_view_key) = self.hex_ui.focused_view {\n            let layout = &self.meta_state.meta.layouts[self.hex_ui.current_layout];\n            if let Some(focused_idx) = layout.iter().position(|k| k == focused_view_key) {\n                let new_idx = if focused_idx == 0 {\n                    layout.iter().count() - 1\n                } else {\n                    focused_idx - 1\n                };\n                if let Some(new_key) = layout.iter().nth(new_idx) {\n                    self.hex_ui.focused_view = Some(new_key);\n                }\n            }\n        }\n    }\n\n    pub(crate) fn focus_next_view_in_layout(&mut self) {\n        if let Some(focused_view_key) = self.hex_ui.focused_view {\n            let layout = &self.meta_state.meta.layouts[self.hex_ui.current_layout];\n            if let Some(focused_idx) = layout.iter().position(|k| k == focused_view_key) {\n                let new_idx = if focused_idx == layout.iter().count() - 1 {\n                    0\n                } else {\n                    focused_idx + 1\n                };\n                if let Some(new_key) = layout.iter().nth(new_idx) {\n                    self.hex_ui.focused_view = Some(new_key);\n                }\n            }\n        }\n    }\n\n    pub(crate) fn focus_first_view_of_key(\n        app_hex_ui: &mut HexUi,\n        app_meta: &Meta,\n        view_key: ViewKey,\n    ) {\n        if let Some(layout_key) = app_meta\n            .layouts\n            .iter()\n            .find_map(|(k, l)| l.contains_view(view_key).then_some(k))\n        {\n            Self::switch_layout(app_hex_ui, app_meta, layout_key);\n            app_hex_ui.focused_view = Some(view_key);\n        }\n    }\n}\n\n/// Perspective manipulation\nimpl App {\n    pub(crate) fn inc_cols(&mut self) {\n        self.col_change_impl(|col| *col += 1);\n    }\n    pub(crate) fn dec_cols(&mut self) {\n        self.col_change_impl(|col| *col -= 1);\n    }\n    pub(crate) fn halve_cols(&mut self) {\n        self.col_change_impl(|col| *col /= 2);\n    }\n    pub(crate) fn double_cols(&mut self) {\n        self.col_change_impl(|col| *col *= 2);\n    }\n    fn col_change_impl(&mut self, f: impl FnOnce(&mut usize)) {\n        if let Some(key) = self.hex_ui.focused_view {\n            let view = &mut self.meta_state.meta.views[key].view;\n            col_change_impl_view_perspective(\n                view,\n                &mut self.meta_state.meta.low.perspectives,\n                &self.meta_state.meta.low.regions,\n                f,\n                self.preferences.col_change_lock_col,\n                self.preferences.col_change_lock_row,\n            );\n        }\n    }\n}\n\n/// Finding things\nimpl App {\n    // Byte offset of a pixel position in the viewport\n    //\n    // Also returns the index of the view the position is from\n    pub fn byte_offset_at_pos(&self, x: i16, y: i16) -> Option<(usize, ViewKey)> {\n        let layout = self.meta_state.meta.layouts.get(self.hex_ui.current_layout)?;\n        for view_key in layout.iter() {\n            if let Some(pos) = self.view_byte_offset_at_pos(view_key, x, y) {\n                return Some((pos, view_key));\n            }\n        }\n        None\n    }\n    pub fn view_byte_offset_at_pos(&self, view_key: ViewKey, x: i16, y: i16) -> Option<usize> {\n        let NamedView { view, .. } = self.meta_state.meta.views.get(view_key)?;\n        view.row_col_offset_of_pos(\n            x,\n            y,\n            &self.meta_state.meta.low.perspectives,\n            &self.meta_state.meta.low.regions,\n        )\n        .map(|[row, col]| {\n            self.meta_state.meta.low.perspectives[view.perspective].byte_offset_of_row_col(\n                row,\n                col,\n                &self.meta_state.meta.low.regions,\n            )\n        })\n    }\n    pub fn view_at_pos(&self, x: ViewportScalar, y: ViewportScalar) -> Option<ViewKey> {\n        let layout = &self.meta_state.meta.layouts[self.hex_ui.current_layout];\n        for row in &layout.view_grid {\n            for key in row {\n                let view = &self.meta_state.meta.views[*key];\n                if view.view.viewport_rect.contains_pos(x, y) {\n                    return Some(*key);\n                }\n            }\n        }\n        None\n    }\n    pub fn view_idx_at_pos(&self, x: i16, y: i16) -> Option<ViewKey> {\n        let layout = &self.meta_state.meta.layouts[self.hex_ui.current_layout];\n        for view_key in layout.iter() {\n            let view = &self.meta_state.meta.views[view_key];\n            if view.view.viewport_rect.contains_pos(x, y) {\n                return Some(view_key);\n            }\n        }\n        None\n    }\n    /// Iterator over the views in the current layout\n    fn active_views(&self) -> impl Iterator<Item = &'_ NamedView> {\n        let layout = &self.meta_state.meta.layouts[self.hex_ui.current_layout];\n        layout.iter().map(|key| &self.meta_state.meta.views[key])\n    }\n    /// Largest visible byte range in the current perspective\n    fn visible_byte_range(&self) -> [usize; 2] {\n        let mut min_lo = self.data.len();\n        let mut max_hi = 0;\n        for view in self.active_views() {\n            let offsets = view.view.offsets(\n                &self.meta_state.meta.low.perspectives,\n                &self.meta_state.meta.low.regions,\n            );\n            let lo = offsets.byte;\n            min_lo = std::cmp::min(min_lo, lo);\n            let hi = lo + view.view.bytes_per_page(&self.meta_state.meta.low.perspectives);\n            max_hi = std::cmp::max(max_hi, hi);\n        }\n        [min_lo, max_hi].map(|v| v.clamp(0, self.data.len()))\n    }\n    pub(crate) fn focused_view_mut(&mut self) -> Option<(ViewKey, &mut View)> {\n        self.hex_ui.focused_view.and_then(|key| {\n            self.meta_state.meta.views.get_mut(key).map(|view| (key, &mut view.view))\n        })\n    }\n    pub(crate) fn row_region(&self, row: usize) -> Option<Region> {\n        let per = Self::focused_perspective(&self.hex_ui, &self.meta_state.meta)?;\n        let per_reg = self.meta_state.meta.low.regions.get(per.region)?.region;\n        // Beginning of the region\n        let beg = per_reg.begin;\n        // Number of columns\n        let cols = per.cols;\n        let row_begin = beg + row * cols;\n        // Regions are inclusive, so we subtract 1\n        let row_end = (row_begin + cols).saturating_sub(1);\n        Some(Region {\n            begin: row_begin,\n            end: row_end,\n        })\n    }\n\n    pub(crate) fn col_offsets(&self, col: usize) -> Option<Vec<usize>> {\n        let per = Self::focused_perspective(&self.hex_ui, &self.meta_state.meta)?;\n        let per_reg = self.meta_state.meta.low.regions.get(per.region)?.region;\n        let beg = per_reg.begin;\n        let end = per_reg.end;\n        let cols = per.cols;\n        let offsets = (beg..=end).step_by(cols).map(|off| off + col).collect();\n        Some(offsets)\n    }\n\n    pub(crate) fn cursor_col_offsets(&self) -> Option<Vec<usize>> {\n        self.row_col_of_cursor().and_then(|[_, col]| self.col_offsets(col))\n    }\n    /// Returns the row and column of the provided byte position, according to focused perspective\n    pub(crate) fn row_col_of_byte_pos(&self, pos: usize) -> Option<[usize; 2]> {\n        Self::focused_perspective(&self.hex_ui, &self.meta_state.meta)\n            .map(|per| calc_perspective_row_col(pos, per, &self.meta_state.meta.low.regions))\n    }\n    /// Returns the byte position of the provided row and column, according to focused perspective\n    pub(crate) fn byte_pos_of_row_col(&self, row: usize, col: usize) -> Option<usize> {\n        Self::focused_perspective(&self.hex_ui, &self.meta_state.meta).map(|per| {\n            calc_perspective_row_col_offset(row, col, per, &self.meta_state.meta.low.regions)\n        })\n    }\n    /// Returns the row and column of the current cursor, according to focused perspective\n    pub(crate) fn row_col_of_cursor(&self) -> Option<[usize; 2]> {\n        self.row_col_of_byte_pos(self.edit_state.cursor)\n    }\n    pub fn focused_perspective<'a>(hex_ui: &HexUi, meta: &'a Meta) -> Option<&'a Perspective> {\n        hex_ui.focused_view.map(|view_key| {\n            let per_key = meta.views[view_key].view.perspective;\n            &meta.low.perspectives[per_key]\n        })\n    }\n    pub fn focused_region<'a>(hex_ui: &HexUi, meta: &'a Meta) -> Option<&'a NamedRegion> {\n        Self::focused_perspective(hex_ui, meta).and_then(|per| meta.low.regions.get(per.region))\n    }\n\n    pub(crate) fn region_key_for_view(&self, view_key: ViewKey) -> RegionKey {\n        let per_key = self.meta_state.meta.views[view_key].view.perspective;\n        self.meta_state.meta.low.perspectives[per_key].region\n    }\n    /// Figure out the byte offset of the row `offset` is on\n    pub(crate) fn find_row_start(&self, offset: usize) -> Option<usize> {\n        match self.row_col_of_byte_pos(offset) {\n            Some([row, _col]) => self.byte_pos_of_row_col(row, 0),\n            None => None,\n        }\n    }\n    /// Figure out the byte offset of the row `offset` is on + end\n    pub(crate) fn find_row_end(&self, offset: usize) -> Option<usize> {\n        Self::focused_perspective(&self.hex_ui, &self.meta_state.meta).map(|per| {\n            let [row, _col] =\n                calc_perspective_row_col(offset, per, &self.meta_state.meta.low.regions);\n            calc_perspective_row_col_offset(\n                row,\n                per.cols.saturating_sub(1),\n                per,\n                &self.meta_state.meta.low.regions,\n            )\n        })\n    }\n}\n\nfn calc_perspective_row_col(pos: usize, per: &Perspective, regions: &RegionMap) -> [usize; 2] {\n    let cols = per.cols;\n    let region_begin = regions[per.region].region.begin;\n    let byte_pos = pos.saturating_sub(region_begin);\n    [byte_pos / cols, byte_pos % cols]\n}\n\nfn calc_perspective_row_col_offset(\n    row: usize,\n    col: usize,\n    per: &Perspective,\n    regions: &RegionMap,\n) -> usize {\n    let region_begin = regions[per.region].region.begin;\n    row * per.cols + col + region_begin\n}\n\n/// Editing\nimpl App {\n    pub(crate) fn mod_byte_at_cursor(&mut self, f: impl FnOnce(&mut u8)) {\n        if let Some(byte) = self.data.get_mut(self.edit_state.cursor) {\n            f(byte);\n            self.data.widen_dirty_region(DamageRegion::Single(self.edit_state.cursor));\n        }\n    }\n\n    pub(crate) fn inc_byte_at_cursor(&mut self) {\n        self.mod_byte_at_cursor(|b| *b = b.wrapping_add(1));\n    }\n\n    pub(crate) fn dec_byte_at_cursor(&mut self) {\n        self.mod_byte_at_cursor(|b| *b = b.wrapping_sub(1));\n    }\n\n    pub(crate) fn inc_byte_or_bytes(&mut self) {\n        let mut any = false;\n        for region in self.hex_ui.selected_regions() {\n            self.data.mod_range(region.to_range(), |byte| {\n                *byte = byte.wrapping_add(1);\n            });\n            any = true;\n        }\n        if !any {\n            self.inc_byte_at_cursor();\n        }\n    }\n\n    pub(crate) fn dec_byte_or_bytes(&mut self) {\n        let mut any = false;\n        for region in self.hex_ui.selected_regions() {\n            self.data.mod_range(region.to_range(), |byte| {\n                *byte = byte.wrapping_sub(1);\n            });\n            any = true;\n        }\n        if !any {\n            self.dec_byte_at_cursor();\n        }\n    }\n}\n\n/// Etc.\nimpl App {\n    pub(crate) fn new(\n        mut args: Args,\n        cfg: Config,\n        font_size: u16,\n        line_spacing: u16,\n        gui: &mut Gui,\n    ) -> anyhow::Result<Self> {\n        if args.recent\n            && let Some(recent) = cfg.recent.most_recent()\n        {\n            args.src = recent.clone();\n        }\n        let mut this = Self {\n            data: Data::default(),\n            edit_state: EditState::default(),\n            input: Input::default(),\n            src_args: SourceArgs::default(),\n            source: None,\n            stream_read_recv: None,\n            cfg,\n            last_reload: Instant::now(),\n            preferences: SessionPrefs::default(),\n            hex_ui: HexUi::default(),\n            meta_state: MetaState::default(),\n            clipboard: arboard::Clipboard::new()?,\n            cmd: Default::default(),\n            backend_cmd: Default::default(),\n            quit_requested: false,\n            plugins: Vec::new(),\n            stream_buffer_size: args.src.stream_buffer_size.unwrap_or(DEFAULT_STREAM_BUFFER_SIZE),\n        };\n        for path in args.load_plugin {\n            // Safety: This will cause UB on a bad plugin. Nothing we can do.\n            //\n            // It's up to the user not to load bad plugins.\n            this.plugins.push(unsafe { PluginContainer::new(path)? });\n        }\n        if args.autosave {\n            this.preferences.auto_save = true;\n        }\n        if let Some(interval_ms) = args.autoreload {\n            if args.autoreload_only_visible {\n                this.preferences.auto_reload = Autoreload::Visible;\n            } else {\n                this.preferences.auto_reload = Autoreload::All;\n            }\n            this.preferences.auto_reload_interval_ms = interval_ms;\n        }\n        match args.new {\n            Some(new_len) => {\n                if let Some(path) = args.src.file {\n                    if path.exists() {\n                        anyhow::bail!(\"Can't use --new for {path:?}: File already exists\");\n                    }\n                    // Set up source for this new file\n                    let f = OpenOptions::new()\n                        .create(true)\n                        .truncate(false)\n                        .read(true)\n                        .write(true)\n                        .open(&path)?;\n                    f.set_len(new_len as u64)?;\n                    this.source = Some(Source::file(f));\n                    this.src_args.file = Some(path);\n                }\n                this.data = Data::clean_from_buf(vec![0; new_len]);\n                // Set clean meta for the newly allocated buffer\n                this.set_new_clean_meta(font_size, line_spacing, args.column_count);\n            }\n            None => {\n                // Set a clean meta, for an empty document\n                this.set_new_clean_meta(font_size, line_spacing, args.column_count);\n                this.load_file_args(\n                    args.src,\n                    args.meta,\n                    &mut gui.msg_dialog,\n                    font_size,\n                    line_spacing,\n                    args.column_count,\n                );\n            }\n        }\n        if let Some(name) = args.layout\n            && !Self::switch_layout_by_name(&mut this.hex_ui, &this.meta_state.meta, &name)\n        {\n            let err = anyhow::anyhow!(\"No layout with name '{name}' found.\");\n            msg_fail(&err, \"Couldn't switch layout\", &mut gui.msg_dialog);\n        }\n        if let Some(name) = args.view\n            && !Self::focus_first_view_of_name(&mut this.hex_ui, &this.meta_state.meta, &name)\n        {\n            let err = anyhow::anyhow!(\"No view with name '{name}' found.\");\n            msg_fail(&err, \"Couldn't focus view\", &mut gui.msg_dialog);\n        }\n        // Set cursor to the beginning of the focused region we ended up with\n        if let Some(reg) = Self::focused_region(&this.hex_ui, &this.meta_state.meta) {\n            this.edit_state.cursor = reg.region.begin;\n        }\n        // Diff against a file if requested\n        if let Some(path) = &args.diff_against {\n            let result = this.diff_with_file(path.clone(), &mut gui.win.file_diff_result);\n            msg_if_fail(result, \"Failed to diff\", &mut gui.msg_dialog);\n        }\n        Ok(this)\n    }\n    /// Reoffset all bookmarks based on the difference between the cursor and `offset`\n    pub(crate) fn reoffset_bookmarks_cursor_diff(&mut self, offset: usize) {\n        #[expect(\n            clippy::cast_possible_wrap,\n            reason = \"We assume that the offset is not greater than isize\"\n        )]\n        let difference = self.edit_state.cursor as isize - offset as isize;\n        for bm in &mut self.meta_state.meta.bookmarks {\n            bm.offset = bm.offset.saturating_add_signed(difference);\n        }\n    }\n\n    pub(crate) fn try_read_stream(&mut self) {\n        let Some(src) = &mut self.source else { return };\n        if !src.attr.stream {\n            return;\n        };\n        let Some(view_key) = self.hex_ui.focused_view else {\n            return;\n        };\n        let view = &self.meta_state.meta.views[view_key].view;\n        let view_byte_offset = view\n            .offsets(\n                &self.meta_state.meta.low.perspectives,\n                &self.meta_state.meta.low.regions,\n            )\n            .byte;\n        let bytes_per_page = view.bytes_per_page(&self.meta_state.meta.low.perspectives);\n        // Don't read past what we need for our current view offset\n        if view_byte_offset + bytes_per_page < self.data.len() {\n            return;\n        }\n        if src.state.stream_end {\n            return;\n        }\n        match &self.stream_read_recv {\n            Some(recv) => match recv.try_recv() {\n                Ok(buf) => {\n                    if buf.is_empty() {\n                        src.state.stream_end = true;\n                    } else {\n                        self.data.extend_from_slice(&buf[..]);\n                        let perspective = &self.meta_state.meta.low.perspectives[view.perspective];\n                        let region =\n                            &mut self.meta_state.meta.low.regions[perspective.region].region;\n                        region.end = self.data.len().saturating_sub(1);\n                    }\n                }\n                Err(e) => match e {\n                    std::sync::mpsc::TryRecvError::Empty => {}\n                    std::sync::mpsc::TryRecvError::Disconnected => self.stream_read_recv = None,\n                },\n            },\n            None => {\n                let (tx, rx) = std::sync::mpsc::channel();\n                let mut src_clone = src.provider.clone();\n                self.stream_read_recv = Some(rx);\n                let buffer_size = self.stream_buffer_size;\n                thread::spawn(move || {\n                    let mut buf = vec![0; buffer_size];\n                    let result = try {\n                        let amount = src_clone.read(&mut buf).how()?;\n                        buf.truncate(amount);\n                        tx.send(buf).how()?;\n                    };\n                    if let Err(e) = result {\n                        per!(\"Stream error: {}\", e);\n                    }\n                });\n            }\n        }\n    }\n\n    /// Called every frame\n    pub(crate) fn update(\n        &mut self,\n        gui: &mut Gui,\n        rw: &mut RenderWindow,\n        lua: &Lua,\n        font_size: u16,\n        line_spacing: u16,\n    ) {\n        if !self.hex_ui.current_layout.is_null() {\n            let layout = &self.meta_state.meta.layouts[self.hex_ui.current_layout];\n            do_auto_layout(\n                layout,\n                &mut self.meta_state.meta.views,\n                &self.hex_ui.hex_iface_rect,\n                &self.meta_state.meta.low.perspectives,\n                &self.meta_state.meta.low.regions,\n            );\n        }\n        if self.preferences.auto_save\n            && self.data.dirty_region.is_some()\n            && let Err(e) = self.save(&mut gui.msg_dialog)\n        {\n            per!(\"Save fail: {}\", e);\n        }\n        if self.preferences.auto_reload.is_active()\n            && self.source.is_some()\n            && self.last_reload.elapsed().as_millis()\n                >= u128::from(self.preferences.auto_reload_interval_ms)\n        {\n            match &self.preferences.auto_reload {\n                Autoreload::Disabled => {}\n                Autoreload::All => {\n                    if msg_if_fail(self.reload(), \"Auto-reload fail\", &mut gui.msg_dialog).is_some()\n                    {\n                        self.preferences.auto_reload = Autoreload::Disabled;\n                    }\n                }\n                Autoreload::Visible => {\n                    if msg_if_fail(\n                        self.reload_visible(),\n                        \"Auto-reload fail\",\n                        &mut gui.msg_dialog,\n                    )\n                    .is_some()\n                    {\n                        self.preferences.auto_reload = Autoreload::Disabled;\n                    }\n                }\n            }\n            self.last_reload = Instant::now();\n        }\n        // Here we perform all queued up `Command`s.\n        self.flush_command_queue(gui, lua, font_size, line_spacing);\n        self.flush_backend_command_queue(rw);\n    }\n\n    pub(crate) fn focused_view_select_all(&mut self) {\n        if let Some(view) = self.hex_ui.focused_view {\n            let p_key = self.meta_state.meta.views[view].view.perspective;\n            let p = &self.meta_state.meta.low.perspectives[p_key];\n            let r = &self.meta_state.meta.low.regions[p.region];\n            self.hex_ui.select_a = Some(r.region.begin);\n            // Don't select more than the data length, even if region is bigger\n            self.hex_ui.select_b = Some(r.region.end.min(self.data.len().saturating_sub(1)));\n        }\n    }\n\n    pub(crate) fn focused_view_select_row(&mut self) {\n        if let Some([row, _]) = self.row_col_of_cursor()\n            && let Some(reg) = self.row_region(row)\n        {\n            // To make behavior consistent with \"select col\", we clear all extra selections beforehand\n            self.hex_ui.extra_selections.clear();\n            self.hex_ui.select_a = Some(reg.begin);\n            self.hex_ui.select_b = Some(reg.end);\n        }\n    }\n\n    pub(crate) fn focused_view_select_col(&mut self) {\n        let Some(offsets) = self.cursor_col_offsets() else {\n            return;\n        };\n        self.hex_ui.extra_selections.clear();\n        let mut offsets = offsets.into_iter();\n        if let Some(off) = offsets.next() {\n            self.hex_ui.select_a = Some(off);\n            self.hex_ui.select_b = Some(off);\n        }\n        for col in offsets {\n            self.hex_ui.extra_selections.push(Region {\n                begin: col,\n                end: col,\n            });\n        }\n    }\n\n    pub(crate) fn diff_with_file(\n        &self,\n        path: PathBuf,\n        file_diff_result_window: &mut FileDiffResultWindow,\n    ) -> anyhow::Result<()> {\n        // FIXME: Skipping ignores changes to bookmarked values that happen later than the first\n        // byte.\n        let file_data = read_source_to_buf(&path, &self.src_args)?;\n        let mut offs = Vec::new();\n        let mut skip = 0;\n        for ((offset, &my_byte), &file_byte) in self.data.iter().enumerate().zip(file_data.iter()) {\n            if skip > 0 {\n                skip -= 1;\n                continue;\n            }\n            if my_byte != file_byte {\n                offs.push(offset);\n            }\n            if let Some((_, bm)) =\n                Meta::bookmark_for_offset(&self.meta_state.meta.bookmarks, offset)\n            {\n                skip = bm.value_type.byte_len() - 1;\n            }\n        }\n        file_diff_result_window.offsets = offs;\n        file_diff_result_window.file_data = file_data;\n        file_diff_result_window.path = path;\n        file_diff_result_window.open.set(true);\n        Ok(())\n    }\n\n    pub(crate) fn call_plugin_method(\n        &mut self,\n        plugin_name: &str,\n        method_name: &str,\n        args: &[Option<hexerator_plugin_api::Value>],\n    ) -> MethodResult {\n        let mut plugins = std::mem::take(&mut self.plugins);\n        let result = 'block: {\n            for plugin in &mut plugins {\n                if plugin_name == plugin.plugin.name() {\n                    break 'block plugin.plugin.on_method_called(method_name, args, self);\n                }\n            }\n            Err(format!(\"Plugin `{plugin_name}` not found.\"))\n        };\n        std::mem::swap(&mut self.plugins, &mut plugins);\n        result\n    }\n\n    pub(crate) fn remove_dangling(&mut self) {\n        self.meta_state.meta.remove_dangling();\n        if self\n            .hex_ui\n            .focused_view\n            .is_some_and(|key| !self.meta_state.meta.views.contains_key(key))\n        {\n            eprintln!(\"Unset dangling focused view\");\n            self.hex_ui.focused_view = None;\n        }\n    }\n}\n\n/// Set up an empty meta with the defaults\npub fn setup_empty_meta(\n    data_len: usize,\n    meta: &mut Meta,\n    font_size: u16,\n    line_spacing: u16,\n    cols: usize,\n) -> LayoutKey {\n    let def_region = meta.low.regions.insert(NamedRegion {\n        name: \"default\".into(),\n        region: Region {\n            begin: 0,\n            end: data_len.saturating_sub(1),\n        },\n        desc: String::new(),\n    });\n    let default_perspective = meta.low.perspectives.insert(Perspective {\n        region: def_region,\n        cols,\n        flip_row_order: false,\n        name: \"default\".to_string(),\n    });\n    let mut layout = Layout {\n        name: \"Default layout\".into(),\n        view_grid: vec![vec![]],\n        margin: default_margin(),\n    };\n    for view in default_views(default_perspective, font_size, line_spacing) {\n        let k = meta.views.insert(view);\n        layout.view_grid[0].push(k);\n    }\n    meta.layouts.insert(layout)\n}\n\npub fn get_clipboard_string(cb: &mut arboard::Clipboard, msg: &mut MessageDialog) -> String {\n    match cb.get_text() {\n        Ok(text) => text,\n        Err(e) => {\n            msg.open(\n                Icon::Error,\n                \"Failed to get text from clipboard\",\n                e.to_string(),\n            );\n            String::new()\n        }\n    }\n}\n\npub fn set_clipboard_string(cb: &mut arboard::Clipboard, msg: &mut MessageDialog, text: &str) {\n    msg_if_fail(cb.set_text(text), \"Failed to set clipboard text\", msg);\n}\n\n#[cfg(target_os = \"linux\")]\nfn load_proc_memory_linux(\n    app: &mut App,\n    pid: sysinfo::Pid,\n    start: usize,\n    size: usize,\n    is_write: bool,\n    msg: &mut MessageDialog,\n    font_size: u16,\n    line_spacing: u16,\n) {\n    app.load_file_args(\n        SourceArgs {\n            file: Some(Path::new(\"/proc/\").join(pid.to_string()).join(\"mem\")),\n            jump: None,\n            hard_seek: Some(start),\n            take: Some(size),\n            read_only: !is_write,\n            stream: false,\n            stream_buffer_size: None,\n            unsafe_mmap: None,\n            mmap_len: None,\n        },\n        None,\n        msg,\n        font_size,\n        line_spacing,\n        None,\n    );\n}\n\n#[cfg(target_os = \"macos\")]\nfn load_proc_memory_macos(\n    app: &mut App,\n    pid: sysinfo::Pid,\n    start: usize,\n    size: usize,\n    is_write: bool,\n    font: &Font,\n    msg: &mut MessageDialog,\n    events: &EventQueue,\n) -> anyhow::Result<()> {\n    app.load_file_args(\n        Args {\n            src: SourceArgs {\n                file: Some(Path::new(\"/proc/\").join(pid.to_string()).join(\"mem\")),\n                jump: None,\n                hard_seek: Some(start),\n                take: Some(size),\n                read_only: !is_write,\n                stream: false,\n            },\n            recent: false,\n            meta: None,\n        },\n        font,\n        msg,\n        events,\n    )\n}\n\npub fn read_source_to_buf(path: &Path, args: &SourceArgs) -> Result<Vec<u8>, anyhow::Error> {\n    let mut f = File::open(path)?;\n    if let &Some(to) = &args.hard_seek {\n        #[expect(\n            clippy::cast_possible_wrap,\n            reason = \"Files bigger than i64::MAX aren't supported\"\n        )]\n        f.seek(SeekFrom::Current(to as i64))?;\n    }\n    #[expect(\n        clippy::cast_possible_truncation,\n        reason = \"On 32 bit, max supported file size is 4 GB\"\n    )]\n    let len = args.take.unwrap_or(f.metadata()?.len() as usize);\n    let mut buf = vec![0; len];\n    f.read_exact(&mut buf)?;\n    Ok(buf)\n}\n\npub fn temp_metafile_backup_path() -> PathBuf {\n    std::env::temp_dir().join(\"hexerator_meta_backup.meta\")\n}\n\npub fn col_change_impl_view_perspective(\n    view: &mut View,\n    perspectives: &mut PerspectiveMap,\n    regions: &RegionMap,\n    f: impl FnOnce(&mut usize),\n    lock_x: bool,\n    lock_y: bool,\n) {\n    let prev_offset = view.offsets(perspectives, regions);\n    f(&mut perspectives[view.perspective].cols);\n    perspectives[view.perspective].clamp_cols(regions);\n    view.scroll_to_byte_offset(prev_offset.byte, perspectives, regions, lock_x, lock_y);\n}\n\npub fn default_views(\n    perspective: PerspectiveKey,\n    font_size: u16,\n    line_spacing: u16,\n) -> Vec<NamedView> {\n    vec![\n        NamedView {\n            view: View::new(\n                ViewKind::Hex(HexData::with_font_size(font_size)),\n                perspective,\n            ),\n            name: \"Default hex\".into(),\n        },\n        NamedView {\n            view: View::new(\n                ViewKind::Text(TextData::with_font_info(line_spacing, font_size)),\n                perspective,\n            ),\n            name: \"Default text\".into(),\n        },\n        NamedView {\n            view: View::new(ViewKind::Block, perspective),\n            name: \"Default block\".into(),\n        },\n    ]\n}\n\n/// Returns if the file was actually loaded.\nfn load_file_from_src_args(\n    src_args: &mut SourceArgs,\n    cfg: &mut Config,\n    source: &mut Option<Source>,\n    data: &mut Data,\n    msg: &mut MessageDialog,\n    cmd: &mut CommandQueue,\n) -> bool {\n    if let Some(file_arg) = &src_args.file {\n        if file_arg.as_os_str() == \"-\" {\n            *source = Some(Source {\n                provider: SourceProvider::Stdin(std::io::stdin()),\n                attr: SourceAttributes {\n                    stream: true,\n                    permissions: SourcePermissions { write: false },\n                },\n                state: SourceState::default(),\n            });\n            cmd.push(Cmd::ProcessSourceChange);\n            true\n        } else {\n            let result = try {\n                let mut file = open_file(file_arg, src_args.read_only)?;\n                if let Some(path) = &mut src_args.file {\n                    match path.canonicalize() {\n                        Ok(canon) => *path = canon,\n                        Err(e) => msg.open(\n                            Icon::Warn,\n                            \"Warning\",\n                            format!(\n                                \"Failed to canonicalize path {}: {}\\n\\\n                             Recent use list might not be able to load it back.\",\n                                path.display(),\n                                e\n                            ),\n                        ),\n                    }\n                }\n                cfg.recent.use_(src_args.clone());\n                if !src_args.stream {\n                    if let Some(mmap_mode) = src_args.unsafe_mmap {\n                        let mut opts = memmap2::MmapOptions::new();\n                        if let Some(len) = src_args.mmap_len {\n                            opts.len(len);\n                        }\n                        // Safety:\n                        //\n                        // Memory mapped file access cannot be made 100% safe, not much we can do here.\n                        //\n                        // The command line option is called `--unsafe-mmap` to reflect this.\n                        *data = unsafe {\n                            match mmap_mode {\n                                crate::args::MmapMode::Cow => {\n                                    Data::new_mmap_mut(opts.map_copy(&file)?)\n                                }\n                                crate::args::MmapMode::DangerousMut => {\n                                    Data::new_mmap_mut(opts.map_mut(&file)?)\n                                }\n                                crate::args::MmapMode::Ro => Data::new_mmap_immut(opts.map(&file)?),\n                            }\n                        };\n                    } else {\n                        *data = Data::clean_from_buf(read_contents(&*src_args, &mut file)?);\n                    }\n                }\n                *source = Some(Source {\n                    provider: SourceProvider::File(file),\n                    attr: SourceAttributes {\n                        stream: src_args.stream,\n                        permissions: SourcePermissions {\n                            write: !src_args.read_only,\n                        },\n                    },\n                    state: SourceState::default(),\n                });\n                cmd.push(Cmd::ProcessSourceChange);\n            };\n            match result {\n                Ok(()) => true,\n                Err(e) => {\n                    if !src_args.read_only && e.kind() == std::io::ErrorKind::PermissionDenied {\n                        eprintln!(\"Failed to open file: {e}. Retrying read-only.\");\n                        src_args.read_only = true;\n                        return load_file_from_src_args(src_args, cfg, source, data, msg, cmd);\n                    }\n                    msg_fail(&e, \"Failed to open file\", msg);\n                    false\n                }\n            }\n        }\n    } else {\n        false\n    }\n}\n\nfn open_file(path: &Path, read_only: bool) -> std::io::Result<File> {\n    OpenOptions::new().read(true).write(!read_only).open(path)\n}\n\npub(crate) fn read_contents(args: &SourceArgs, file: &mut File) -> std::io::Result<Vec<u8>> {\n    let seek = args.hard_seek.unwrap_or(0);\n    file.seek(SeekFrom::Start(seek as u64))?;\n    let mut data = Vec::new();\n    match args.take {\n        Some(amount) => (&*file).take(amount as u64).read_to_end(&mut data)?,\n        None => file.read_to_end(&mut data)?,\n    };\n    Ok(data)\n}\n"
  },
  {
    "path": "src/args.rs",
    "content": "use {\n    crate::parse_radix::parse_guess_radix,\n    clap::Parser,\n    serde::{Deserialize, Serialize},\n    std::path::PathBuf,\n};\n\n/// Hexerator: Versatile hex editor\n#[derive(Parser, Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]\npub struct Args {\n    /// Arguments relating to the source to open\n    #[clap(flatten)]\n    pub src: SourceArgs,\n    /// Open most recently used file\n    #[arg(long)]\n    pub recent: bool,\n    /// Load this metafile\n    #[arg(long, value_name = \"path\")]\n    pub meta: Option<PathBuf>,\n    /// Show version information and exit\n    #[arg(long)]\n    pub version: bool,\n    /// Start with debug logging enabled\n    #[arg(long)]\n    pub debug: bool,\n    /// Spawn and open memory of a command with arguments (must be last option)\n    #[arg(long, value_name=\"command\", allow_hyphen_values=true, num_args=1..)]\n    pub spawn_command: Vec<String>,\n    #[arg(long, value_name = \"name\")]\n    /// When spawning a command, open the process list with this filter, rather than selecting a pid\n    pub look_for_proc: Option<String>,\n    /// Automatically reload the source for the current buffer in millisecond intervals (default:250)\n    #[arg(long, value_name=\"interval\", default_missing_value=\"250\", num_args=0..=1)]\n    pub autoreload: Option<u32>,\n    /// Only autoreload the data visible in the current layout\n    #[arg(long)]\n    pub autoreload_only_visible: bool,\n    /// Automatically save if there is an edited region in the file\n    #[arg(long)]\n    pub autosave: bool,\n    /// Open this layout on startup instead of the default\n    #[arg(long, value_name = \"name\")]\n    pub layout: Option<String>,\n    /// Focus the first instance of this view on startup\n    #[arg(long, value_name = \"name\")]\n    pub view: Option<String>,\n    #[arg(long)]\n    /// Load a dynamic library plugin at startup\n    pub load_plugin: Vec<PathBuf>,\n    /// Allocate a new (zero-filled) buffer. Also creates the provided file argument if it doesn't exist.\n    #[arg(long, value_name = \"length\")]\n    pub new: Option<usize>,\n    /// Diff against this file\n    #[arg(long, value_name = \"path\", alias = \"diff-with\")]\n    pub diff_against: Option<PathBuf>,\n    /// Set the initial column count of the default perspective\n    #[arg(short = 'c', long = \"col\")]\n    pub column_count: Option<usize>,\n}\n\n/// Arguments for opening a source (file/stream/process/etc)\n#[derive(Parser, Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Default)]\npub struct SourceArgs {\n    /// The file to read\n    pub file: Option<PathBuf>,\n    /// Jump to offset on startup\n    #[arg(short = 'j', long=\"jump\", value_name=\"offset\", value_parser = parse_guess_radix::<usize>)]\n    pub jump: Option<usize>,\n    /// Seek to offset, consider it beginning of the file in the editor\n    #[arg(long, value_name=\"offset\", value_parser = parse_guess_radix::<usize>)]\n    pub hard_seek: Option<usize>,\n    /// Read only this many bytes\n    #[arg(long, value_name = \"bytes\", value_parser = parse_guess_radix::<usize>)]\n    pub take: Option<usize>,\n    /// Open file as read-only, without writing privileges\n    #[arg(long)]\n    pub read_only: bool,\n    /// Specify source as a streaming source (for example, standard streams).\n    /// Sets read-only attribute.\n    #[arg(long)]\n    pub stream: bool,\n    /// The buffer size in bytes to use for reading when streaming\n    #[arg(long)]\n    #[serde(default)]\n    pub stream_buffer_size: Option<usize>,\n    /// Try to open the source using mmap rather than load into a buffer\n    #[serde(default)]\n    #[arg(long, value_name = \"mode\")]\n    pub unsafe_mmap: Option<MmapMode>,\n    /// Assume the memory mapped file is of this length (might be needed for looking at block devices, etc.)\n    #[serde(default)]\n    #[arg(long, value_name = \"len\")]\n    pub mmap_len: Option<usize>,\n}\n\n/// How the memory mapping should operate\n#[derive(\n    Clone,\n    Copy,\n    clap::ValueEnum,\n    Debug,\n    Serialize,\n    Deserialize,\n    PartialEq,\n    Eq,\n    strum::IntoStaticStr,\n    strum::EnumIter,\n    Default,\n)]\npub enum MmapMode {\n    /// Read-only memory map.\n    /// Note: Some features may not work, as Hexerator was designed for a mutable data buffer.\n    #[default]\n    Ro,\n    /// Copy-on-write memory map.\n    /// Changes are only visible locally.\n    Cow,\n    /// Mutable memory map.\n    /// *WARNING*: Any edits will immediately take effect. THEY CANNOT BE UNDONE.\n    DangerousMut,\n}\n"
  },
  {
    "path": "src/backend/sfml.rs",
    "content": "use {\n    crate::{\n        color::{RgbColor, RgbaColor},\n        view::{ViewportScalar, ViewportVec},\n    },\n    egui_sf2g::sf2g::graphics::Color,\n};\n\nimpl From<Color> for RgbaColor {\n    fn from(Color { r, g, b, a }: Color) -> Self {\n        Self { r, g, b, a }\n    }\n}\n\nimpl From<RgbaColor> for Color {\n    fn from(RgbaColor { r, g, b, a }: RgbaColor) -> Self {\n        Self { r, g, b, a }\n    }\n}\n\nimpl From<RgbColor> for Color {\n    fn from(src: RgbColor) -> Self {\n        Self {\n            r: src.r,\n            g: src.g,\n            b: src.b,\n            a: 255,\n        }\n    }\n}\n\nimpl TryFrom<sf2g::system::Vector2<i32>> for ViewportVec {\n    type Error = <ViewportScalar as TryFrom<i32>>::Error;\n\n    fn try_from(sf_vec: sf2g::system::Vector2<i32>) -> Result<Self, Self::Error> {\n        Ok(Self {\n            x: sf_vec.x.try_into()?,\n            y: sf_vec.y.try_into()?,\n        })\n    }\n}\n"
  },
  {
    "path": "src/backend.rs",
    "content": "#[cfg(feature = \"backend-sfml\")]\nmod sfml;\n"
  },
  {
    "path": "src/color.rs",
    "content": "use serde::{Deserialize, Serialize};\n\n#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]\npub struct RgbaColor {\n    pub r: u8,\n    pub g: u8,\n    pub b: u8,\n    pub a: u8,\n}\nimpl RgbaColor {\n    pub(crate) fn with_as_egui_mut(&mut self, f: impl FnOnce(&mut egui::Color32)) {\n        let mut ec = self.to_egui();\n        f(&mut ec);\n        *self = Self::from_egui(ec);\n    }\n    fn from_egui(c: egui::Color32) -> Self {\n        Self {\n            r: c.r(),\n            g: c.g(),\n            b: c.b(),\n            a: c.a(),\n        }\n    }\n    fn to_egui(self) -> egui::Color32 {\n        egui::Color32::from_rgba_premultiplied(self.r, self.g, self.b, self.a)\n    }\n}\n\npub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> RgbaColor {\n    RgbaColor { r, g, b, a }\n}\n\n#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]\npub struct RgbColor {\n    pub r: u8,\n    pub g: u8,\n    pub b: u8,\n}\n\nimpl RgbColor {\n    pub const WHITE: Self = rgb(255, 255, 255);\n\n    pub fn invert(&self) -> Self {\n        rgb(!self.r, !self.g, !self.b)\n    }\n\n    pub(crate) fn cap_brightness(&self, limit: u8) -> Self {\n        Self {\n            r: self.r.min(limit),\n            g: self.g.min(limit),\n            b: self.b.min(limit),\n        }\n    }\n}\n\npub const fn rgb(r: u8, g: u8, b: u8) -> RgbColor {\n    RgbColor { r, g, b }\n}\n"
  },
  {
    "path": "src/config.rs",
    "content": "use {\n    crate::{args::SourceArgs, result_ext::AnyhowConv as _},\n    anyhow::Context as _,\n    directories::ProjectDirs,\n    egui_fontcfg::CustomFontPaths,\n    recently_used_list::RecentlyUsedList,\n    serde::{Deserialize, Serialize},\n    std::{\n        collections::{BTreeMap, HashMap},\n        path::PathBuf,\n    },\n};\n\n#[derive(Serialize, Deserialize)]\npub struct Config {\n    pub recent: RecentlyUsedList<SourceArgs>,\n    pub style: Style,\n    /// filepath->meta associations\n    #[serde(default)]\n    pub meta_assocs: MetaAssocs,\n    #[serde(default = \"default_vsync\")]\n    pub vsync: bool,\n    #[serde(default)]\n    pub fps_limit: u32,\n    #[serde(default)]\n    pub pinned_dirs: Vec<PinnedDir>,\n    #[serde(default)]\n    pub custom_font_paths: CustomFontPaths,\n    #[serde(default)]\n    pub font_families: BTreeMap<egui::FontFamily, Vec<String>>,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct PinnedDir {\n    pub path: PathBuf,\n    pub label: String,\n}\n\nconst fn default_vsync() -> bool {\n    true\n}\n\npub type MetaAssocs = HashMap<PathBuf, PathBuf>;\n\n#[derive(Serialize, Deserialize, Default)]\npub struct Style {\n    pub font_sizes: FontSizes,\n}\n\n#[derive(Serialize, Deserialize)]\npub struct FontSizes {\n    pub heading: u8,\n    pub body: u8,\n    pub monospace: u8,\n    pub button: u8,\n    pub small: u8,\n}\n\nimpl Default for FontSizes {\n    fn default() -> Self {\n        Self {\n            small: 10,\n            body: 14,\n            button: 14,\n            heading: 16,\n            monospace: 14,\n        }\n    }\n}\n\nconst DEFAULT_RECENT_CAPACITY: usize = 16;\n\nimpl Default for Config {\n    fn default() -> Self {\n        let mut recent = RecentlyUsedList::default();\n        recent.set_capacity(DEFAULT_RECENT_CAPACITY);\n        Self {\n            recent,\n            style: Style::default(),\n            meta_assocs: HashMap::default(),\n            fps_limit: 0,\n            vsync: default_vsync(),\n            pinned_dirs: Vec::new(),\n            custom_font_paths: Default::default(),\n            font_families: Default::default(),\n        }\n    }\n}\n\npub struct LoadedConfig {\n    pub config: Config,\n    /// If `Some`, saving this config file will overwrite an old one that couldn't be loaded\n    pub old_config_err: Option<anyhow::Error>,\n}\n\nimpl Config {\n    pub fn load_or_default() -> anyhow::Result<LoadedConfig> {\n        let proj_dirs = project_dirs().context(\"Failed to get project dirs\")?;\n        let cfg_dir = proj_dirs.config_dir();\n        if !cfg_dir.exists() {\n            std::fs::create_dir_all(cfg_dir)?;\n        }\n        let cfg_file = cfg_dir.join(FILENAME);\n        if !cfg_file.exists() {\n            Ok(LoadedConfig {\n                config: Self::default(),\n                old_config_err: None,\n            })\n        } else {\n            let result = try {\n                let cfg_bytes = std::fs::read(cfg_file).how()?;\n                rmp_serde::from_slice(&cfg_bytes).how()?\n            };\n            match result {\n                Ok(cfg) => Ok(LoadedConfig {\n                    config: cfg,\n                    old_config_err: None,\n                }),\n                Err(e) => Ok(LoadedConfig {\n                    config: Self::default(),\n                    old_config_err: Some(e),\n                }),\n            }\n        }\n    }\n    pub fn save(&self) -> anyhow::Result<()> {\n        let bytes = rmp_serde::to_vec(self)?;\n        let proj_dirs = project_dirs().context(\"Failed to get project dirs\")?;\n        let cfg_dir = proj_dirs.config_dir();\n        std::fs::write(cfg_dir.join(FILENAME), bytes)?;\n        Ok(())\n    }\n}\n\npub fn project_dirs() -> Option<ProjectDirs> {\n    ProjectDirs::from(\"\", \"crumblingstatue\", \"hexerator\")\n}\n\npub trait ProjectDirsExt {\n    fn color_theme_path(&self) -> PathBuf;\n}\n\nimpl ProjectDirsExt for ProjectDirs {\n    fn color_theme_path(&self) -> PathBuf {\n        self.config_dir().join(\"egui_colors_theme.pal\")\n    }\n}\n\nconst FILENAME: &str = \"hexerator.cfg\";\n"
  },
  {
    "path": "src/damage_region.rs",
    "content": "pub enum DamageRegion {\n    Single(usize),\n    Range(std::ops::Range<usize>),\n    RangeInclusive(std::ops::RangeInclusive<usize>),\n}\n\nimpl DamageRegion {\n    pub(crate) fn begin(&self) -> usize {\n        match self {\n            Self::Single(offset) => *offset,\n            Self::Range(range) => range.start,\n            Self::RangeInclusive(range) => *range.start(),\n        }\n    }\n\n    pub(crate) fn end(&self) -> usize {\n        match self {\n            Self::Single(offset) => *offset,\n            Self::Range(range) => range.end - 1,\n            Self::RangeInclusive(range) => *range.end(),\n        }\n    }\n}\n\nimpl From<std::ops::RangeInclusive<usize>> for DamageRegion {\n    fn from(range: std::ops::RangeInclusive<usize>) -> Self {\n        Self::RangeInclusive(range)\n    }\n}\n"
  },
  {
    "path": "src/data.rs",
    "content": "use {\n    crate::{damage_region::DamageRegion, meta::region::Region},\n    std::ops::{Deref, DerefMut},\n};\n\n/// The data we are viewing/editing\n#[derive(Default, Debug)]\npub struct Data {\n    data: Option<DataProvider>,\n    /// The region that was changed compared to the source\n    pub dirty_region: Option<Region>,\n    /// Original data length. Compared with current data length to detect truncation.\n    pub orig_data_len: usize,\n}\n\nenum DataProvider {\n    Vec(Vec<u8>),\n    MmapMut(memmap2::MmapMut),\n    MmapImmut(memmap2::Mmap),\n}\n\nimpl std::fmt::Debug for DataProvider {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Vec(..) => f.write_str(\"Vec\"),\n            Self::MmapMut(..) => f.write_str(\"MmapMut\"),\n            Self::MmapImmut(..) => f.write_str(\"MmapImmut\"),\n        }\n    }\n}\n\nimpl Data {\n    pub(crate) fn clean_from_buf(buf: Vec<u8>) -> Self {\n        Self {\n            orig_data_len: buf.len(),\n            data: Some(DataProvider::Vec(buf)),\n            dirty_region: None,\n        }\n    }\n    pub(crate) fn new_mmap_mut(mmap: memmap2::MmapMut) -> Self {\n        Self {\n            orig_data_len: mmap.len(),\n            data: Some(DataProvider::MmapMut(mmap)),\n            dirty_region: None,\n        }\n    }\n    pub(crate) fn new_mmap_immut(mmap: memmap2::Mmap) -> Self {\n        Self {\n            orig_data_len: mmap.len(),\n            data: Some(DataProvider::MmapImmut(mmap)),\n            dirty_region: None,\n        }\n    }\n    /// Drop any expensive allocations and reset to \"empty\" state\n    pub(crate) fn close(&mut self) {\n        self.data = None;\n        self.dirty_region = None;\n    }\n    pub(crate) fn widen_dirty_region(&mut self, damage: DamageRegion) {\n        match &mut self.dirty_region {\n            Some(dirty_region) => {\n                if damage.begin() < dirty_region.begin {\n                    dirty_region.begin = damage.begin();\n                }\n                if damage.begin() > dirty_region.end {\n                    dirty_region.end = damage.begin();\n                }\n                let end = damage.end();\n                {\n                    if end < dirty_region.begin {\n                        gamedebug_core::per!(\"TODO: logic error in widen_dirty_region\");\n                        return;\n                    }\n                    if end > dirty_region.end {\n                        dirty_region.end = end;\n                    }\n                }\n            }\n            None => {\n                self.dirty_region = Some(Region {\n                    begin: damage.begin(),\n                    end: damage.end(),\n                });\n            }\n        }\n    }\n    /// Clears the dirty region (asserts data is same as source), and sets length same as source\n    pub(crate) fn undirty(&mut self) {\n        self.dirty_region = None;\n        self.orig_data_len = self.len();\n    }\n\n    pub(crate) fn resize(&mut self, new_len: usize, value: u8) {\n        match &mut self.data {\n            Some(DataProvider::Vec(v)) => v.resize(new_len, value),\n            etc => {\n                eprintln!(\"Data::resize: Unimplemented for {etc:?}\");\n            }\n        }\n    }\n\n    pub(crate) fn extend_from_slice(&mut self, slice: &[u8]) {\n        match &mut self.data {\n            Some(DataProvider::Vec(v)) => v.extend_from_slice(slice),\n            etc => {\n                eprintln!(\"Data::extend_from_slice: Unimplemented for {etc:?}\");\n            }\n        }\n    }\n\n    pub(crate) fn drain(&mut self, range: std::ops::Range<usize>) {\n        match &mut self.data {\n            Some(DataProvider::Vec(v)) => {\n                v.drain(range);\n            }\n            etc => {\n                eprintln!(\"Data::drain: Unimplemented for {etc:?}\");\n            }\n        }\n    }\n\n    pub(crate) fn zero_fill_region(&mut self, region: Region) {\n        let range = region.begin..=region.end;\n        if let Some(data) = self.get_mut(range.clone()) {\n            data.fill(0);\n            self.widen_dirty_region(DamageRegion::RangeInclusive(range));\n        }\n    }\n\n    pub(crate) fn reload_from_file(\n        &mut self,\n        src_args: &crate::args::SourceArgs,\n        file: &mut std::fs::File,\n    ) -> anyhow::Result<()> {\n        match &mut self.data {\n            Some(DataProvider::Vec(buf)) => {\n                *buf = crate::app::read_contents(src_args, file)?;\n            }\n            etc => anyhow::bail!(\"Reload not supported for {etc:?}\"),\n        }\n        self.dirty_region = None;\n        Ok(())\n    }\n\n    pub(crate) fn mod_range(\n        &mut self,\n        range: std::ops::RangeInclusive<usize>,\n        mut f: impl FnMut(&mut u8),\n    ) {\n        for byte in self.get_mut(range.clone()).into_iter().flatten() {\n            f(byte);\n        }\n        self.widen_dirty_region(range.into());\n    }\n}\n\nimpl Deref for Data {\n    type Target = [u8];\n\n    fn deref(&self) -> &Self::Target {\n        match &self.data {\n            Some(DataProvider::Vec(v)) => v,\n            Some(DataProvider::MmapMut(map)) => map,\n            Some(DataProvider::MmapImmut(map)) => map,\n            None => &[],\n        }\n    }\n}\n\nimpl DerefMut for Data {\n    fn deref_mut(&mut self) -> &mut Self::Target {\n        match &mut self.data {\n            Some(DataProvider::Vec(v)) => v,\n            Some(DataProvider::MmapMut(map)) => map,\n            Some(DataProvider::MmapImmut(_)) => &mut [],\n            None => &mut [],\n        }\n    }\n}\n"
  },
  {
    "path": "src/dec_conv.rs",
    "content": "fn byte_10_digits(byte: u8) -> [u8; 3] {\n    [byte / 100, (byte % 100) / 10, byte % 10]\n}\n\n#[test]\nfn test_byte_10_digits() {\n    assert_eq!(byte_10_digits(255), [2, 5, 5]);\n}\n\npub fn byte_to_dec_digits(byte: u8) -> [u8; 3] {\n    const TABLE: &[u8; 10] = b\"0123456789\";\n\n    let [a, b, c] = byte_10_digits(byte);\n    [TABLE[a as usize], TABLE[b as usize], TABLE[c as usize]]\n}\n\n#[test]\nfn test_byte_to_dec_digits() {\n    let pairs = [\n        (255, b\"255\"),\n        (0, b\"000\"),\n        (1, b\"001\"),\n        (15, b\"015\"),\n        (16, b\"016\"),\n        (154, b\"154\"),\n        (167, b\"167\"),\n        (6, b\"006\"),\n        (64, b\"064\"),\n        (127, b\"127\"),\n        (128, b\"128\"),\n        (129, b\"129\"),\n    ];\n    for (byte, hex) in pairs {\n        assert_eq!(byte_to_dec_digits(byte), *hex);\n    }\n}\n"
  },
  {
    "path": "src/edit_buffer.rs",
    "content": "use gamedebug_core::per;\n\n#[derive(Debug, Default, Clone)]\npub struct EditBuffer {\n    pub buf: Vec<u8>,\n    pub cursor: u16,\n    /// Whether this edit buffer has been edited\n    pub dirty: bool,\n}\n\nimpl EditBuffer {\n    pub(crate) fn resize(&mut self, new_size: u16) {\n        self.buf.resize(usize::from(new_size), 0);\n    }\n    /// Enter a byte. Returns if editing is \"finished\" (at end)\n    pub(crate) fn enter_byte(&mut self, byte: u8) -> bool {\n        self.dirty = true;\n        self.buf[self.cursor as usize] = byte;\n        self.cursor += 1;\n        if usize::from(self.cursor) >= self.buf.len() {\n            self.reset();\n            true\n        } else {\n            false\n        }\n    }\n\n    pub fn reset(&mut self) {\n        self.cursor = 0;\n        self.dirty = false;\n    }\n\n    pub(crate) fn update_from_string(&mut self, s: &str) {\n        let bytes = s.as_bytes();\n        self.buf[..bytes.len()].copy_from_slice(bytes);\n    }\n    /// Returns whether the cursor could be moved any further\n    pub(crate) fn move_cursor_back(&mut self) -> bool {\n        if self.cursor == 0 {\n            false\n        } else {\n            self.cursor -= 1;\n            true\n        }\n    }\n    /// Move the cursor to the end\n    #[expect(\n        clippy::cast_possible_truncation,\n        reason = \"Buffer is never bigger than u16::MAX\"\n    )]\n    pub(crate) fn move_cursor_end(&mut self) {\n        self.cursor = (self.buf.len() - 1) as u16;\n    }\n\n    /// Returns whether the cursor could be moved any further\n    #[expect(\n        clippy::cast_possible_truncation,\n        reason = \"Buffer is never bigger than u16::MAX\"\n    )]\n    pub(crate) fn move_cursor_forward(&mut self) -> bool {\n        if self.cursor >= self.buf.len() as u16 - 1 {\n            false\n        } else {\n            per!(\"Moving cursor forward, no problem\");\n            self.cursor += 1;\n            true\n        }\n    }\n\n    pub(crate) fn move_cursor_begin(&mut self) {\n        self.cursor = 0;\n    }\n}\n"
  },
  {
    "path": "src/find_util.rs",
    "content": "pub fn find_hex_string(\n    hex_string: &str,\n    haystack: &[u8],\n    mut f: impl FnMut(usize),\n) -> anyhow::Result<()> {\n    let needle = parse_hex_string(hex_string)?;\n    for offset in memchr::memmem::find_iter(haystack, &needle) {\n        f(offset);\n    }\n    Ok(())\n}\n\nenum HexStringSepKind {\n    Comma,\n    Whitespace,\n    Dense,\n}\n\nfn detect_hex_string_sep_kind(hex_string: &str) -> HexStringSepKind {\n    if hex_string.contains(',') {\n        HexStringSepKind::Comma\n    } else if hex_string.contains(char::is_whitespace) {\n        HexStringSepKind::Whitespace\n    } else {\n        HexStringSepKind::Dense\n    }\n}\n\nfn chunks_2(input: &str) -> impl Iterator<Item = anyhow::Result<&str>> {\n    input\n        .as_bytes()\n        .as_chunks::<2>()\n        .0\n        .iter()\n        .map(|pair| std::str::from_utf8(pair).map_err(anyhow::Error::from))\n}\n\npub fn parse_hex_string(hex_string: &str) -> anyhow::Result<Vec<u8>> {\n    match detect_hex_string_sep_kind(hex_string) {\n        HexStringSepKind::Comma => {\n            hex_string.split(',').map(|tok| parse_hex_token(tok.trim())).collect()\n        }\n        HexStringSepKind::Whitespace => {\n            hex_string.split_whitespace().map(parse_hex_token).collect()\n        }\n        HexStringSepKind::Dense => chunks_2(hex_string).map(|tok| parse_hex_token(tok?)).collect(),\n    }\n}\n\nfn parse_hex_token(tok: &str) -> anyhow::Result<u8> {\n    Ok(u8::from_str_radix(tok, 16)?)\n}\n\n#[test]\nfn test_parse_hex_string() {\n    assert_eq!(\n        parse_hex_string(\"de ad be ef\").unwrap(),\n        vec![0xde, 0xad, 0xbe, 0xef]\n    );\n    assert_eq!(\n        parse_hex_string(\"de, ad, be, ef\").unwrap(),\n        vec![0xde, 0xad, 0xbe, 0xef]\n    );\n    assert_eq!(\n        parse_hex_string(\"deadbeef\").unwrap(),\n        vec![0xde, 0xad, 0xbe, 0xef]\n    );\n}\n"
  },
  {
    "path": "src/gui/bottom_panel.rs",
    "content": "use {\n    super::{Gui, dialogs::JumpDialog, egui_ui_ext::EguiResponseExt as _},\n    crate::{\n        app::{App, interact_mode::InteractMode},\n        meta::find_most_specific_region_for_offset,\n        shell::msg_if_fail,\n        util::human_size,\n        view::ViewportVec,\n    },\n    constcat::concat,\n    egui::{Align, Color32, DragValue, Stroke, TextFormat, TextStyle, Ui, text::LayoutJob},\n    egui_phosphor::regular as ic,\n    slotmap::Key as _,\n};\n\nconst L_SCROLL: &str = concat!(ic::MOUSE_SCROLL, \" scroll\");\n\npub fn ui(ui: &mut Ui, app: &mut App, mouse_pos: ViewportVec, gui: &mut Gui) {\n    ui.horizontal(|ui| {\n        let job = key_label(ui, \"F1\", \"View\");\n        if ui\n            .selectable_label(app.hex_ui.interact_mode == InteractMode::View, job)\n            .clicked()\n        {\n            app.hex_ui.interact_mode = InteractMode::View;\n        }\n        ui.style_mut().visuals.selection.bg_fill = Color32::from_rgb(168, 150, 32);\n        let job = key_label(ui, \"F2\", \"Edit\");\n        if ui\n            .selectable_label(app.hex_ui.interact_mode == InteractMode::Edit, job)\n            .clicked()\n        {\n            app.hex_ui.interact_mode = InteractMode::Edit;\n        }\n        ui.separator();\n        let data_len = app.data.len();\n        if data_len != 0\n            && let Some(view_key) = app.hex_ui.focused_view\n        {\n            let view = &mut app.meta_state.meta.views[view_key].view;\n            let per = match app.meta_state.meta.low.perspectives.get_mut(view.perspective) {\n                Some(per) => per,\n                None => {\n                    ui.label(\"Invalid perspective key\");\n                    return;\n                }\n            };\n            ui.label(\"offset\");\n            ui.add(DragValue::new(\n                &mut app.meta_state.meta.low.regions[per.region].region.begin,\n            ));\n            ui.label(\"columns\");\n            ui.add(DragValue::new(&mut per.cols));\n            let offsets = view.offsets(\n                &app.meta_state.meta.low.perspectives,\n                &app.meta_state.meta.low.regions,\n            );\n            let re = ui.button(L_SCROLL);\n            if re.clicked() {\n                gui.show_quick_scroll_popup ^= true;\n            }\n            #[expect(\n                clippy::cast_precision_loss,\n                reason = \"Precision is good until 52 bits (more than reasonable)\"\n            )]\n            let mut ratio = offsets.byte as f64 / data_len as f64;\n            if gui.show_quick_scroll_popup {\n                let avail_w = ui.available_width();\n                egui::Window::new(\"quick_scroll_popup\")\n                    .resizable(false)\n                    .title_bar(false)\n                    .fixed_pos(re.rect.right_top())\n                    .show(ui.ctx(), |ui| {\n                        ui.spacing_mut().slider_width = avail_w * 0.8;\n                        let re = ui.add(\n                            egui::Slider::new(&mut ratio, 0.0..=1.0)\n                                .custom_formatter(|n, _| format!(\"{:.2}%\", n * 100.)),\n                        );\n                        if re.changed() {\n                            // This is used for a rough scroll, so lossy conversion is to be expected\n                            #[expect(\n                                clippy::cast_possible_truncation,\n                                clippy::cast_precision_loss,\n                                clippy::cast_sign_loss\n                            )]\n                            let new_off = (app.data.len() as f64 * ratio) as usize;\n                            view.scroll_to_byte_offset(\n                                new_off,\n                                &app.meta_state.meta.low.perspectives,\n                                &app.meta_state.meta.low.regions,\n                                false,\n                                true,\n                            );\n                        }\n                        ui.horizontal(|ui| {\n                            ui.label(human_size(offsets.byte));\n                            if ui.button(\"Close\").clicked() {\n                                gui.show_quick_scroll_popup = false;\n                            }\n                        });\n                    });\n            }\n            ui.label(format!(\n                \"row {} col {} byte {} ({:.2}%)\",\n                offsets.row,\n                offsets.col,\n                offsets.byte,\n                ratio * 100.0\n            ))\n            .on_hover_text_deferred(|| human_size(offsets.byte));\n        }\n        ui.separator();\n        let [row, col] = app.row_col_of_cursor().unwrap_or([0, 0]);\n        let mut text = egui::RichText::new(format!(\n            \"cursor: {} ({:x}) [r{row} c{col}]\",\n            app.edit_state.cursor, app.edit_state.cursor,\n        ));\n        let out_of_bounds = app.edit_state.cursor >= app.data.len();\n        let cursor_end = app.edit_state.cursor == app.data.len().saturating_sub(1);\n        let cursor_begin = app.edit_state.cursor == 0;\n        if out_of_bounds {\n            text = text.color(Color32::RED);\n        } else if cursor_end {\n            text = text.color(Color32::YELLOW);\n        } else if cursor_begin {\n            text = text.color(Color32::GREEN);\n        }\n        let re = ui.label(text);\n        re.context_menu(|ui| {\n            if ui.button(\"Copy\").clicked() {\n                let result = app.clipboard.set_text(app.edit_state.cursor.to_string());\n                msg_if_fail(result, \"Failed to set clipboard text\", &mut gui.msg_dialog);\n            }\n            if ui.button(\"Copy absolute\").on_hover_text(\"Hard seek + cursor\").clicked() {\n                let result = app.clipboard.set_text(\n                    (app.edit_state.cursor + app.src_args.hard_seek.unwrap_or(0)).to_string(),\n                );\n                msg_if_fail(result, \"Failed to set clipboard text\", &mut gui.msg_dialog);\n            }\n        });\n        if re.clicked() {\n            Gui::add_dialog(&mut gui.dialogs, JumpDialog::default());\n        }\n        if out_of_bounds {\n            re.on_hover_text(\"Cursor is out of bounds\");\n        } else if cursor_end {\n            re.on_hover_text(\"Cursor is at end of document\");\n        } else if cursor_begin {\n            re.on_hover_text(\"Cursor is at beginning\");\n        } else {\n            re.on_hover_text_deferred(|| human_size(app.edit_state.cursor));\n        }\n        if let Some(label) = app\n            .meta_state\n            .meta\n            .bookmarks\n            .iter()\n            .find_map(|bm| (bm.offset == app.edit_state.cursor).then_some(bm.label.as_str()))\n        {\n            ui.label(egui::RichText::new(label).color(Color32::from_rgb(150, 170, 40)));\n        }\n        if let Some(region) = find_most_specific_region_for_offset(\n            &app.meta_state.meta.low.regions,\n            app.edit_state.cursor,\n        ) {\n            let reg = &app.meta_state.meta.low.regions[region];\n            region_label(ui, &reg.name).context_menu(|ui| {\n                if ui.button(\"Select\").clicked() {\n                    app.hex_ui.select_a = Some(reg.region.begin);\n                    app.hex_ui.select_b = Some(reg.region.end);\n                }\n            });\n        }\n        if !app.hex_ui.current_layout.is_null()\n            && let Some((offset, _view_idx)) = app.byte_offset_at_pos(mouse_pos.x, mouse_pos.y)\n        {\n            let [row, col] = app.row_col_of_byte_pos(offset).unwrap_or([0, 0]);\n            ui.label(format!(\"mouse: {offset} ({offset:x}) [r{row} c{col}]\"));\n            if let Some(region) =\n                find_most_specific_region_for_offset(&app.meta_state.meta.low.regions, offset)\n            {\n                region_label(ui, &app.meta_state.meta.low.regions[region].name);\n            }\n        }\n        ui.with_layout(egui::Layout::right_to_left(Align::Center), |ui| {\n            let mut txt = egui::RichText::new(format!(\"File size: {}\", app.data.len()));\n            let truncated = app.data.len() != app.data.orig_data_len;\n            if truncated {\n                txt = txt.color(Color32::RED);\n            }\n            let label = egui::Label::new(txt).sense(egui::Sense::click());\n            let mut label_re = ui.add(label).on_hover_ui(|ui| {\n                ui.label(\"Click to copy\");\n                ui.label(format!(\"Human size: {}\", human_size(app.data.len())));\n            });\n            if truncated {\n                label_re = label_re.on_hover_text_deferred(|| {\n                    format!(\"Length changed, orig.: {}\", app.data.orig_data_len)\n                });\n            }\n            if label_re.clicked() {\n                crate::app::set_clipboard_string(\n                    &mut app.clipboard,\n                    &mut gui.msg_dialog,\n                    &app.data.len().to_string(),\n                );\n            }\n        });\n    });\n}\n\nfn region_label(ui: &mut Ui, name: &str) -> egui::Response {\n    let label =\n        egui::Label::new(egui::RichText::new(format!(\"[{name}]\")).color(Color32::LIGHT_BLUE))\n            .sense(egui::Sense::click());\n    ui.add(label)\n}\n\n/// A key \"box\" and then some text. Like `[F1] View`\nfn key_label(ui: &Ui, key_text: &str, label_text: &str) -> LayoutJob {\n    let mut job = LayoutJob::default();\n    let style = ui.style();\n    let body_font = TextStyle::Body.resolve(style);\n    job.append(\n        key_text,\n        0.0,\n        TextFormat {\n            font_id: body_font.clone(),\n            color: style.visuals.widgets.active.fg_stroke.color,\n            background: style.visuals.code_bg_color,\n            italics: false,\n            underline: Stroke::NONE,\n            strikethrough: Stroke::NONE,\n            valign: Align::Center,\n            ..Default::default()\n        },\n    );\n    job.append(\n        label_text,\n        10.0,\n        TextFormat::simple(body_font, style.visuals.widgets.active.fg_stroke.color),\n    );\n    job\n}\n"
  },
  {
    "path": "src/gui/command.rs",
    "content": "//! This module is similar in purpose to [`crate::app::command`].\n//!\n//! See that module for more information.\n\nuse {\n    super::Gui,\n    crate::shell::msg_fail,\n    std::{collections::VecDeque, process::Command},\n    sysinfo::ProcessesToUpdate,\n};\n\npub enum GCmd {\n    OpenPerspectiveWindow,\n    /// Spawn a command with optional arguments. Must not be an empty vector.\n    SpawnCommand {\n        args: Vec<String>,\n        /// If `Some`, don't focus a pid, just filter for this process in the list.\n        ///\n        /// The idea is that if your command spawns a child process, it might not spawn immediately,\n        /// so the user can wait for it to appear on the process list, with the applied filter.\n        look_for_proc: Option<String>,\n    },\n}\n\n/// Gui command queue.\n///\n/// Push operations with `push`, and call [`Gui::flush_command_queue`] when you have\n/// exclusive access to the [`Gui`].\n///\n/// [`Gui::flush_command_queue`] is called automatically every frame, if you don't need to perform the operations sooner.\n#[derive(Default)]\npub struct GCommandQueue {\n    inner: VecDeque<GCmd>,\n}\n\nimpl GCommandQueue {\n    pub fn push(&mut self, command: GCmd) {\n        self.inner.push_back(command);\n    }\n}\n\nimpl Gui {\n    /// Flush the [`GCommandQueue`] and perform all operations queued up.\n    ///\n    /// Automatically called every frame, but can be called manually if operations need to be\n    /// performed sooner.\n    pub fn flush_command_queue(&mut self) {\n        while let Some(cmd) = self.cmd.inner.pop_front() {\n            perform_command(self, cmd);\n        }\n    }\n}\n\nfn perform_command(gui: &mut Gui, cmd: GCmd) {\n    match cmd {\n        GCmd::OpenPerspectiveWindow => gui.win.perspectives.open.set(true),\n        GCmd::SpawnCommand {\n            mut args,\n            look_for_proc,\n        } => {\n            let cmd = args.remove(0);\n            match Command::new(cmd).args(args).spawn() {\n                Ok(child) => {\n                    gui.win.open_process.open.set(true);\n                    match look_for_proc {\n                        Some(procname) => {\n                            gui.win\n                                .open_process\n                                .sys\n                                .refresh_processes(ProcessesToUpdate::All, true);\n                            gui.win.open_process.filters.proc_name = procname;\n                        }\n                        None => {\n                            gui.win.open_process.selected_pid =\n                                Some(sysinfo::Pid::from_u32(child.id()));\n                        }\n                    }\n                }\n                Err(e) => {\n                    msg_fail(&e, \"Failed to spawn command\", &mut gui.msg_dialog);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/dialogs/auto_save_reload.rs",
    "content": "use {\n    crate::{app::App, gui::Dialog, session_prefs::Autoreload},\n    mlua::Lua,\n};\n\n#[derive(Debug)]\npub struct AutoSaveReloadDialog;\n\nimpl Dialog for AutoSaveReloadDialog {\n    fn title(&self) -> &str {\n        \"Auto save/reload\"\n    }\n\n    fn ui(\n        &mut self,\n        ui: &mut egui::Ui,\n        app: &mut App,\n        _gui: &mut crate::gui::Gui,\n        _lua: &Lua,\n        _font_size: u16,\n        _line_spacing: u16,\n    ) -> bool {\n        egui::ComboBox::from_label(\"Auto reload\")\n            .selected_text(app.preferences.auto_reload.label())\n            .show_ui(ui, |ui| {\n                ui.selectable_value(\n                    &mut app.preferences.auto_reload,\n                    Autoreload::Disabled,\n                    Autoreload::Disabled.label(),\n                );\n                ui.selectable_value(\n                    &mut app.preferences.auto_reload,\n                    Autoreload::All,\n                    Autoreload::All.label(),\n                );\n                ui.selectable_value(\n                    &mut app.preferences.auto_reload,\n                    Autoreload::Visible,\n                    Autoreload::Visible.label(),\n                );\n            });\n        ui.horizontal(|ui| {\n            ui.label(\"Interval (ms)\");\n            ui.add(egui::DragValue::new(\n                &mut app.preferences.auto_reload_interval_ms,\n            ));\n        });\n        ui.separator();\n        ui.checkbox(&mut app.preferences.auto_save, \"Auto save\")\n            .on_hover_text(\"Save every time an editing action is finished\");\n        ui.separator();\n        !(ui.button(\"Close (enter/esc)\").clicked()\n            || ui.input(|inp| inp.key_pressed(egui::Key::Escape))\n            || ui.input(|inp| inp.key_pressed(egui::Key::Enter)))\n    }\n}\n"
  },
  {
    "path": "src/gui/dialogs/jump.rs",
    "content": "use {\n    crate::{\n        app::App,\n        gui::Dialog,\n        parse_radix::{Relativity, parse_offset_maybe_relative},\n        shell::msg_fail,\n    },\n    mlua::Lua,\n};\n\n#[derive(Debug, Default)]\npub struct JumpDialog {\n    string_buf: String,\n    absolute: bool,\n    just_opened: bool,\n}\n\nimpl Dialog for JumpDialog {\n    fn title(&self) -> &str {\n        \"Jump\"\n    }\n\n    fn on_open(&mut self) {\n        self.just_opened = true;\n    }\n\n    fn ui(\n        &mut self,\n        ui: &mut egui::Ui,\n        app: &mut App,\n        gui: &mut crate::gui::Gui,\n        _lua: &Lua,\n        _font_size: u16,\n        _line_spacing: u16,\n    ) -> bool {\n        ui.horizontal(|ui| {\n            ui.label(\"Offset\");\n            let re = ui.text_edit_singleline(&mut self.string_buf);\n            if self.just_opened {\n                re.request_focus();\n            }\n        });\n        self.just_opened = false;\n        ui.label(\n            \"Accepts both decimal and hexadecimal.\\nPrefix with `0x` to force hex.\\n\\\n        Prefix with `+` to add to current offset, `-` to subtract\",\n        );\n        if let Some(hard_seek) = app.src_args.hard_seek {\n            ui.checkbox(&mut self.absolute, \"Absolute\")\n                .on_hover_text(\"Subtract the offset from hard-seek\");\n            let label = format!(\"hard-seek is at {hard_seek} (0x{hard_seek:X})\");\n            ui.text_edit_multiline(&mut &label[..]);\n        }\n        if ui.input(|inp| inp.key_pressed(egui::Key::Enter)) {\n            // Just close the dialog without error on empty text input\n            if self.string_buf.trim().is_empty() {\n                return false;\n            }\n            match parse_offset_maybe_relative(&self.string_buf) {\n                Ok((offset, relativity)) => {\n                    let offset = match relativity {\n                        Relativity::Absolute => {\n                            if let Some(hard_seek) = app.src_args.hard_seek\n                                && self.absolute\n                            {\n                                offset.saturating_sub(hard_seek)\n                            } else {\n                                offset\n                            }\n                        }\n                        Relativity::RelAdd => app.edit_state.cursor.saturating_add(offset),\n                        Relativity::RelSub => app.edit_state.cursor.saturating_sub(offset),\n                    };\n                    app.edit_state.cursor = offset;\n                    app.center_view_on_offset(offset);\n                    app.hex_ui.flash_cursor();\n                    false\n                }\n                Err(e) => {\n                    msg_fail(&e, \"Failed to parse offset\", &mut gui.msg_dialog);\n                    true\n                }\n            }\n        } else {\n            !(ui.input(|inp| inp.key_pressed(egui::Key::Escape)))\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/dialogs/lua_color.rs",
    "content": "use {\n    crate::{app::App, gui::Dialog, value_color::ColorMethod},\n    mlua::{Function, Lua},\n};\n\npub struct LuaColorDialog {\n    script: String,\n    err_string: String,\n    auto_exec: bool,\n}\n\nimpl Default for LuaColorDialog {\n    fn default() -> Self {\n        const DEFAULT_SCRIPT: &str =\n            include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/lua/color.lua\"));\n        Self {\n            script: DEFAULT_SCRIPT.into(),\n            err_string: String::new(),\n            auto_exec: Default::default(),\n        }\n    }\n}\n\nimpl Dialog for LuaColorDialog {\n    fn title(&self) -> &str {\n        \"Lua color\"\n    }\n\n    fn ui(\n        &mut self,\n        ui: &mut egui::Ui,\n        app: &mut App,\n        _gui: &mut crate::gui::Gui,\n        lua: &Lua,\n        _font_size: u16,\n        _line_spacing: u16,\n    ) -> bool {\n        let color_data = match app.hex_ui.focused_view {\n            Some(view_key) => {\n                let view = &mut app.meta_state.meta.views[view_key].view;\n                match &mut view.presentation.color_method {\n                    ColorMethod::Custom(color_data) => &mut color_data.0,\n                    _ => {\n                        ui.label(\"Please select \\\"Custom\\\" as color scheme for the current view\");\n                        return !ui.button(\"Close\").clicked();\n                    }\n                }\n            }\n            None => {\n                ui.label(\"No active view\");\n                return !ui.button(\"Close\").clicked();\n            }\n        };\n        egui::TextEdit::multiline(&mut self.script)\n            .code_editor()\n            .desired_width(f32::INFINITY)\n            .show(ui);\n        ui.horizontal(|ui| {\n            if ui.button(\"Execute\").clicked() || self.auto_exec {\n                let chunk = lua.load(&self.script);\n                let res = try {\n                    let fun = chunk.eval::<Function>()?;\n                    for (i, c) in color_data.iter_mut().enumerate() {\n                        let rgb: [u8; 3] = fun.call((i,))?;\n                        *c = rgb;\n                    }\n                };\n                if let Err(e) = res {\n                    self.err_string = e.to_string();\n                } else {\n                    self.err_string.clear();\n                }\n            }\n            ui.checkbox(&mut self.auto_exec, \"Auto execute\");\n        });\n        if !self.err_string.is_empty() {\n            ui.label(egui::RichText::new(&self.err_string).color(egui::Color32::RED));\n        }\n        if ui.button(\"Close\").clicked() {\n            return false;\n        }\n        true\n    }\n}\n"
  },
  {
    "path": "src/gui/dialogs/lua_fill.rs",
    "content": "use {\n    crate::{app::App, gui::Dialog, shell::msg_if_fail},\n    egui_code_editor::{CodeEditor, Syntax},\n    mlua::{Function, Lua},\n    std::time::Instant,\n};\n\n#[derive(Debug, Default)]\npub struct LuaFillDialog {\n    result_info_string: String,\n    err: bool,\n}\n\nimpl Dialog for LuaFillDialog {\n    fn title(&self) -> &str {\n        \"Lua fill\"\n    }\n\n    fn ui(\n        &mut self,\n        ui: &mut egui::Ui,\n        app: &mut App,\n        gui: &mut crate::gui::Gui,\n        lua: &Lua,\n        _font_size: u16,\n        _line_spacing: u16,\n    ) -> bool {\n        let Some(sel) = app.hex_ui.selection() else {\n            ui.heading(\"No active selection\");\n            return !ui.button(\"Close\").clicked();\n        };\n        let ctrl_enter =\n            ui.input_mut(|inp| inp.consume_key(egui::Modifiers::CTRL, egui::Key::Enter));\n\n        let ctrl_s = ui.input_mut(|inp| inp.consume_key(egui::Modifiers::CTRL, egui::Key::S));\n        if ctrl_s {\n            msg_if_fail(\n                app.save(&mut gui.msg_dialog),\n                \"Failed to save\",\n                &mut gui.msg_dialog,\n            );\n        }\n        egui::ScrollArea::vertical()\n            // 100.0 is an estimation of ui size below.\n            // If we don't subtract that, the text edit tries to expand\n            // beyond window height\n            .max_height(ui.available_height() - 100.0)\n            .show(ui, |ui| {\n                CodeEditor::default()\n                    .with_syntax(Syntax::lua())\n                    .show(ui, &mut app.meta_state.meta.misc.fill_lua_script);\n            });\n        if ui.button(\"Execute\").clicked() || ctrl_enter {\n            let start_time = Instant::now();\n            let chunk = lua.load(&app.meta_state.meta.misc.fill_lua_script);\n            let res = try {\n                let f = chunk.eval::<Function>()?;\n                for (i, b) in app.data[sel.begin..=sel.end].iter_mut().enumerate() {\n                    *b = f.call((i, *b))?;\n                }\n                app.data.dirty_region = Some(sel);\n            };\n            if let Err(e) = res {\n                self.result_info_string = e.to_string();\n                self.err = true;\n            } else {\n                self.result_info_string =\n                    format!(\"Script took {} ms\", start_time.elapsed().as_millis());\n                self.err = false;\n            }\n        }\n        if app.data.dirty_region.is_some() {\n            ui.label(\n                egui::RichText::new(\"Unsaved changes\")\n                    .italics()\n                    .color(egui::Color32::YELLOW)\n                    .code(),\n            );\n        } else {\n            ui.label(egui::RichText::new(\"No unsaved changes\").color(egui::Color32::GREEN).code());\n        }\n        ui.label(\"ctrl+enter to execute, ctrl+s to save file\");\n        if !self.result_info_string.is_empty() {\n            if self.err {\n                ui.label(egui::RichText::new(&self.result_info_string).color(egui::Color32::RED));\n            } else {\n                ui.label(&self.result_info_string);\n            }\n        }\n        true\n    }\n    fn has_close_button(&self) -> bool {\n        true\n    }\n}\n"
  },
  {
    "path": "src/gui/dialogs/pattern_fill.rs",
    "content": "use {\n    crate::{\n        app::App,\n        damage_region::DamageRegion,\n        find_util,\n        gui::{Dialog, message_dialog::Icon},\n        slice_ext::SliceExt as _,\n    },\n    mlua::Lua,\n};\n\n#[derive(Debug, Default)]\npub struct PatternFillDialog {\n    pattern_string: String,\n    just_opened: bool,\n}\n\nimpl Dialog for PatternFillDialog {\n    fn title(&self) -> &str {\n        \"Selection pattern fill\"\n    }\n\n    fn on_open(&mut self) {\n        self.just_opened = true;\n    }\n\n    fn ui(\n        &mut self,\n        ui: &mut egui::Ui,\n        app: &mut App,\n        gui: &mut crate::gui::Gui,\n        _lua: &Lua,\n        _font_size: u16,\n        _line_spacing: u16,\n    ) -> bool {\n        let re = ui.add(\n            egui::TextEdit::singleline(&mut self.pattern_string)\n                .hint_text(\"Hex pattern (e.g. `00 ff 00`)\"),\n        );\n        if self.just_opened {\n            re.request_focus();\n        }\n        self.just_opened = false;\n        if ui.input(|inp| inp.key_pressed(egui::Key::Enter)) {\n            let values: Result<Vec<u8>, _> = find_util::parse_hex_string(&self.pattern_string);\n            match values {\n                Ok(values) => {\n                    for reg in app.hex_ui.selected_regions() {\n                        let range = reg.to_range();\n                        let Some(data_slice) = app.data.get_mut(range.clone()) else {\n                            gui.msg_dialog.open(Icon::Error, \"Pattern fill error\", format!(\"Invalid range for fill.\\nRequested range: {range:?}\\nData length: {}\", app.data.len()));\n                            return false;\n                        };\n                        data_slice.pattern_fill(&values);\n                        app.data.widen_dirty_region(DamageRegion::RangeInclusive(range));\n                    }\n                    false\n                }\n                Err(e) => {\n                    gui.msg_dialog.open(Icon::Error, \"Fill parse error\", e.to_string());\n                    true\n                }\n            }\n        } else {\n            true\n        }\n    }\n    fn has_close_button(&self) -> bool {\n        true\n    }\n}\n"
  },
  {
    "path": "src/gui/dialogs/truncate.rs",
    "content": "use {\n    crate::{app::App, gui::Dialog, meta::region::Region},\n    egui::{Button, DragValue},\n    mlua::Lua,\n};\n\npub struct TruncateDialog {\n    begin: usize,\n    end: usize,\n}\n\nimpl TruncateDialog {\n    pub fn new(data_len: usize, selection: Option<Region>) -> Self {\n        let (begin, end) = match selection {\n            Some(region) => (region.begin, region.end),\n            None => (0, data_len.saturating_sub(1)),\n        };\n        Self { begin, end }\n    }\n}\n\nimpl Dialog for TruncateDialog {\n    fn title(&self) -> &str {\n        \"Truncate/Extend\"\n    }\n\n    fn ui(\n        &mut self,\n        ui: &mut egui::Ui,\n        app: &mut App,\n        _gui: &mut crate::gui::Gui,\n        _lua: &Lua,\n        _font_size: u16,\n        _line_spacing: u16,\n    ) -> bool {\n        ui.horizontal(|ui| {\n            ui.label(\"Begin\");\n            ui.add(DragValue::new(&mut self.begin).range(0..=self.end.saturating_sub(1)));\n            if ui\n                .add_enabled(\n                    self.begin != app.edit_state.cursor,\n                    Button::new(\"From cursor\"),\n                )\n                .clicked()\n            {\n                self.begin = app.edit_state.cursor;\n            }\n        });\n        ui.horizontal(|ui| {\n            ui.label(\"End\");\n            ui.add(DragValue::new(&mut self.end));\n            if ui\n                .add_enabled(\n                    self.end != app.edit_state.cursor,\n                    Button::new(\"From cursor\"),\n                )\n                .clicked()\n            {\n                self.end = app.edit_state.cursor;\n            }\n        });\n        let new_len = (self.end + 1) - self.begin;\n        let mut text = egui::RichText::new(format!(\"New length: {new_len}\"));\n        match new_len.cmp(&app.data.orig_data_len) {\n            std::cmp::Ordering::Less => text = text.color(egui::Color32::RED),\n            std::cmp::Ordering::Equal => {}\n            std::cmp::Ordering::Greater => text = text.color(egui::Color32::YELLOW),\n        }\n        ui.label(text);\n        if let Some(sel) = app.hex_ui.selection() {\n            if ui\n                .add_enabled(\n                    !(sel.begin == self.begin && sel.end == self.end),\n                    Button::new(\"From selection\"),\n                )\n                .clicked()\n            {\n                self.begin = sel.begin;\n                self.end = sel.end;\n            }\n        } else {\n            ui.add_enabled(false, Button::new(\"From selection\"));\n        }\n        ui.separator();\n        let text = egui::RichText::new(\"⚠ Truncate/Extend ⚠\").color(egui::Color32::RED);\n        let mut retain = true;\n        ui.horizontal(|ui| {\n            if ui\n                .button(text)\n                .on_hover_text(\"This will change the length of the data\")\n                .clicked()\n            {\n                app.data.resize(self.end + 1, 0);\n                app.data.drain(0..self.begin);\n                app.hex_ui.clear_selections();\n                app.data.dirty_region = Some(Region {\n                    begin: 0,\n                    end: app.data.len(),\n                });\n            }\n            if ui.button(\"Close\").clicked() {\n                retain = false;\n            }\n        });\n        retain\n    }\n}\n"
  },
  {
    "path": "src/gui/dialogs/x86_asm.rs",
    "content": "use {\n    crate::{app::App, gui::Dialog},\n    egui::Button,\n    iced_x86::{Decoder, Formatter as _, NasmFormatter},\n    mlua::Lua,\n};\n\npub struct X86AsmDialog {\n    decoded: Vec<DecodedInstr>,\n    bitness: u32,\n}\n\nimpl X86AsmDialog {\n    pub fn new() -> Self {\n        Self {\n            decoded: Vec::new(),\n            bitness: 64,\n        }\n    }\n}\n\nimpl Dialog for X86AsmDialog {\n    fn title(&self) -> &str {\n        \"X86 assembly\"\n    }\n\n    fn ui(\n        &mut self,\n        ui: &mut egui::Ui,\n        app: &mut App,\n        _gui: &mut crate::gui::Gui,\n        _lua: &Lua,\n        _font_size: u16,\n        _line_spacing: u16,\n    ) -> bool {\n        let mut retain = true;\n        egui::ScrollArea::vertical()\n            .auto_shrink(false)\n            .max_height(320.0)\n            .show(ui, |ui| {\n                egui::Grid::new(\"asm_grid\").num_columns(2).show(ui, |ui| {\n                    for instr in &self.decoded {\n                        let Some(sel_begin) = app.hex_ui.selection().map(|sel| sel.begin) else {\n                            ui.label(\"No selection\");\n                            return;\n                        };\n                        let instr_off = instr.offset + sel_begin;\n                        if ui.link(instr_off.to_string()).clicked() {\n                            app.search_focus(instr_off);\n                        }\n                        ui.label(&instr.string);\n                        ui.end_row();\n                    }\n                });\n            });\n        ui.separator();\n        match app.hex_ui.selection() {\n            Some(sel) => {\n                if ui.button(\"Disassemble\").clicked() {\n                    self.decoded = disasm(&app.data[sel.begin..=sel.end], self.bitness);\n                }\n            }\n            None => {\n                ui.add_enabled(false, Button::new(\"Disassemble\"));\n            }\n        }\n        ui.horizontal(|ui| {\n            ui.label(\"Bitness\");\n            ui.radio_value(&mut self.bitness, 16, \"16\");\n            ui.radio_value(&mut self.bitness, 32, \"32\");\n            ui.radio_value(&mut self.bitness, 64, \"64\");\n        });\n        if ui.button(\"Close\").clicked() {\n            retain = false;\n        }\n        retain\n    }\n}\n\nstruct DecodedInstr {\n    string: String,\n    offset: usize,\n}\n\nfn disasm(data: &[u8], bitness: u32) -> Vec<DecodedInstr> {\n    let mut decoder = Decoder::new(bitness, data, 0);\n    let mut fmt = NasmFormatter::default();\n    let mut vec = Vec::new();\n    while decoder.can_decode() {\n        let offset = decoder.position();\n        let instr = decoder.decode();\n        let mut string = String::new();\n        fmt.format(&instr, &mut string);\n        vec.push(DecodedInstr { string, offset });\n    }\n    vec\n}\n"
  },
  {
    "path": "src/gui/dialogs.rs",
    "content": "mod auto_save_reload;\nmod jump;\nmod lua_color;\nmod lua_fill;\npub mod pattern_fill;\nmod truncate;\nmod x86_asm;\n\npub use {\n    auto_save_reload::AutoSaveReloadDialog, jump::JumpDialog, lua_color::LuaColorDialog,\n    lua_fill::LuaFillDialog, pattern_fill::PatternFillDialog, truncate::TruncateDialog,\n    x86_asm::X86AsmDialog,\n};\n"
  },
  {
    "path": "src/gui/egui_ui_ext.rs",
    "content": "pub trait EguiResponseExt {\n    fn on_hover_text_deferred<F, R>(self, text_fun: F) -> Self\n    where\n        F: FnOnce() -> R,\n        R: Into<egui::WidgetText>;\n}\n\nimpl EguiResponseExt for egui::Response {\n    fn on_hover_text_deferred<F, R>(self, text_fun: F) -> Self\n    where\n        F: FnOnce() -> R,\n        R: Into<egui::WidgetText>,\n    {\n        // Yoinked from egui source\n        self.on_hover_ui(|ui| {\n            // Prevent `Area` auto-sizing from shrinking tooltips with dynamic content.\n            // See https://github.com/emilk/egui/issues/5167\n            ui.set_max_width(ui.spacing().tooltip_width);\n\n            ui.add(egui::Label::new(text_fun()));\n        })\n    }\n}\n"
  },
  {
    "path": "src/gui/file_ops.rs",
    "content": "use {\n    crate::{\n        app::App,\n        args::{MmapMode, SourceArgs},\n        gui::{message_dialog::MessageDialog, windows::FileDiffResultWindow},\n        meta::{ViewKey, region::Region},\n        result_ext::AnyhowConv as _,\n        shell::{msg_fail, msg_if_fail},\n        source::Source,\n        util::human_size_u64,\n        value_color::{self, ColorMethod},\n    },\n    anyhow::Context as _,\n    egui_file_dialog::FileDialog,\n    std::{\n        io::Write as _,\n        path::{Path, PathBuf},\n    },\n    strum::IntoEnumIterator as _,\n};\n\nstruct EntInfo {\n    meta: std::io::Result<std::fs::Metadata>,\n    mime: Option<&'static str>,\n}\n\ntype PreviewCache = PathCache<EntInfo>;\n\npub struct FileOps {\n    pub dialog: FileDialog,\n    pub op: Option<FileOp>,\n    preview_cache: PreviewCache,\n    file_dialog_source_args: SourceArgs,\n}\n\nimpl Default for FileOps {\n    fn default() -> Self {\n        Self {\n            dialog: FileDialog::new()\n                .anchor(egui::Align2::CENTER_CENTER, egui::vec2(0., 0.))\n                .allow_path_edit_to_save_file_without_extension(true),\n            op: Default::default(),\n            preview_cache: PathCache::default(),\n            file_dialog_source_args: SourceArgs::default(),\n        }\n    }\n}\n\npub struct PathCache<V> {\n    key: PathBuf,\n    value: Option<V>,\n}\n\nimpl<V> Default for PathCache<V> {\n    fn default() -> Self {\n        Self {\n            key: PathBuf::default(),\n            value: None,\n        }\n    }\n}\n\nimpl<V> PathCache<V> {\n    fn get_or_compute<F: FnOnce(&Path) -> V>(&mut self, k: &Path, f: F) -> &V {\n        if self.key != k {\n            self.key = k.to_path_buf();\n            self.value.insert(f(k))\n        } else {\n            self.value.get_or_insert_with(|| {\n                self.key = k.to_path_buf();\n                f(k)\n            })\n        }\n    }\n}\n\n#[derive(Debug)]\npub enum FileOp {\n    LoadMetaFile,\n    LoadFile,\n    LoadPaletteForView(ViewKey),\n    LoadPaletteFromImageForView(ViewKey),\n    DiffWithFile,\n    LoadLuaScript,\n    SavePaletteForView(ViewKey),\n    SaveFileAs,\n    SaveLuaScript,\n    SaveMetaFileAs,\n    SaveSelectionToFile(Region),\n}\n\nimpl FileOps {\n    pub fn update(\n        &mut self,\n        ctx: &egui::Context,\n        app: &mut App,\n        msg: &mut MessageDialog,\n        file_diff_result_window: &mut FileDiffResultWindow,\n        font_size: u16,\n        line_spacing: u16,\n    ) {\n        self.dialog.update_with_right_panel_ui(ctx, &mut |ui, dia| {\n            let src_args = self\n                .op\n                .as_ref()\n                .is_some_and(|op| matches!(op, FileOp::LoadFile))\n                .then_some(&mut self.file_dialog_source_args);\n            right_panel_ui(ui, dia, &mut self.preview_cache, src_args);\n        });\n        if let Some(path) = self.dialog.take_picked()\n            && let Some(op) = self.op.take()\n        {\n            match op {\n                FileOp::LoadMetaFile => {\n                    msg_if_fail(\n                        app.consume_meta_from_file(path, false),\n                        \"Failed to load metafile\",\n                        msg,\n                    );\n                }\n                FileOp::LoadFile => {\n                    self.file_dialog_source_args.file = Some(path);\n                    app.load_file_args(\n                        self.file_dialog_source_args.clone(),\n                        None,\n                        msg,\n                        font_size,\n                        line_spacing,\n                        None,\n                    );\n                }\n                FileOp::LoadPaletteForView(key) => match value_color::load_palette(&path) {\n                    Ok(pal) => {\n                        let view = &mut app.meta_state.meta.views[key].view;\n                        view.presentation.color_method = ColorMethod::Custom(Box::new(pal));\n                    }\n                    Err(e) => msg_fail(&e, \"Failed to load pal\", msg),\n                },\n                FileOp::LoadPaletteFromImageForView(key) => {\n                    let view = &mut app.meta_state.meta.views[key].view;\n                    let ColorMethod::Custom(pal) = &mut view.presentation.color_method else {\n                        return;\n                    };\n                    let result = try {\n                        let img = image::open(path).context(\"Failed to load image\")?.to_rgb8();\n                        let (width, height) = (img.width(), img.height());\n                        let sel = app.hex_ui.selection().context(\"Missing app selection\")?;\n                        let mut i = 0;\n                        for y in 0..height {\n                            for x in 0..width {\n                                let &image::Rgb(rgb) = img.get_pixel(x, y);\n                                let Some(byte) = app.data.get(sel.begin + i) else {\n                                    break;\n                                };\n                                pal.0[*byte as usize] = rgb;\n                                i += 1;\n                            }\n                        }\n                    };\n                    msg_if_fail(result, \"Failed to load palette from reference image\", msg);\n                }\n                FileOp::DiffWithFile => {\n                    msg_if_fail(\n                        app.diff_with_file(path, file_diff_result_window),\n                        \"Failed to diff\",\n                        msg,\n                    );\n                }\n                FileOp::LoadLuaScript => {\n                    let res = try {\n                        app.meta_state.meta.misc.exec_lua_script =\n                            std::fs::read_to_string(path).how()?;\n                    };\n                    msg_if_fail(res, \"Failed to load script\", msg);\n                }\n                FileOp::SavePaletteForView(key) => {\n                    let view = &mut app.meta_state.meta.views[key].view;\n                    let ColorMethod::Custom(pal) = &view.presentation.color_method else {\n                        return;\n                    };\n                    msg_if_fail(\n                        value_color::save_palette(pal, &path),\n                        \"Failed to save pal\",\n                        msg,\n                    );\n                }\n                FileOp::SaveFileAs => {\n                    let result = try {\n                        let mut f = std::fs::OpenOptions::new()\n                            .create(true)\n                            .truncate(true)\n                            .read(true)\n                            .write(true)\n                            .open(&path)\n                            .how()?;\n                        f.write_all(&app.data).how()?;\n                        app.source = Some(Source::file(f));\n                        app.src_args.file = Some(path);\n                        app.cfg.recent.use_(SourceArgs {\n                            file: app.src_args.file.clone(),\n                            jump: None,\n                            hard_seek: None,\n                            take: None,\n                            read_only: false,\n                            stream: false,\n                            stream_buffer_size: None,\n                            unsafe_mmap: None,\n                            mmap_len: None,\n                        });\n                    };\n                    msg_if_fail(result, \"Failed to save as\", msg);\n                }\n                FileOp::SaveLuaScript => {\n                    msg_if_fail(\n                        std::fs::write(path, &app.meta_state.meta.misc.exec_lua_script),\n                        \"Failed to save script\",\n                        msg,\n                    );\n                }\n                FileOp::SaveMetaFileAs => {\n                    msg_if_fail(\n                        app.save_meta_to_file(path, false),\n                        \"Failed to save metafile\",\n                        msg,\n                    );\n                }\n                FileOp::SaveSelectionToFile(sel) => {\n                    let result = std::fs::write(path, &app.data[sel.begin..=sel.end]);\n                    msg_if_fail(result, \"Failed to save selection to file\", msg);\n                }\n            }\n        }\n    }\n    pub fn load_file(&mut self, source_file: Option<&Path>) {\n        if let Some(path) = source_file\n            && let Some(parent) = path.parent()\n        {\n            let cfg = self.dialog.config_mut();\n            parent.clone_into(&mut cfg.initial_directory);\n        }\n        self.dialog.pick_file();\n        self.op = Some(FileOp::LoadFile);\n    }\n    pub fn load_meta_file(&mut self) {\n        self.dialog.pick_file();\n        self.op = Some(FileOp::LoadMetaFile);\n    }\n\n    pub fn load_palette_for_view(&mut self, key: ViewKey) {\n        self.dialog.pick_file();\n        self.op = Some(FileOp::LoadPaletteForView(key));\n    }\n\n    pub fn load_palette_from_image_for_view(&mut self, view_key: ViewKey) {\n        self.dialog.pick_file();\n        self.op = Some(FileOp::LoadPaletteFromImageForView(view_key));\n    }\n\n    pub fn diff_with_file(&mut self, source_file: Option<&Path>) {\n        if let Some(path) = source_file\n            && let Some(parent) = path.parent()\n        {\n            self.dialog.config_mut().initial_directory = parent.to_owned();\n        }\n        self.dialog.pick_file();\n        self.op = Some(FileOp::DiffWithFile);\n    }\n\n    pub fn load_lua_script(&mut self) {\n        self.dialog.pick_file();\n        self.op = Some(FileOp::LoadLuaScript);\n    }\n\n    pub fn save_palette_for_view(&mut self, view_key: ViewKey) {\n        self.dialog.save_file();\n        self.op = Some(FileOp::SavePaletteForView(view_key));\n    }\n\n    pub(crate) fn save_file_as(&mut self) {\n        self.dialog.save_file();\n        self.op = Some(FileOp::SaveFileAs);\n    }\n\n    pub(crate) fn save_lua_script(&mut self) {\n        self.dialog.save_file();\n        self.op = Some(FileOp::SaveLuaScript);\n    }\n\n    pub(crate) fn save_metafile_as(&mut self) {\n        self.dialog.save_file();\n        self.op = Some(FileOp::SaveMetaFileAs);\n    }\n\n    pub(crate) fn save_selection_to_file(&mut self, region: Region) {\n        self.dialog.save_file();\n        self.op = Some(FileOp::SaveSelectionToFile(region));\n    }\n}\n\nfn right_panel_ui(\n    ui: &mut egui::Ui,\n    dia: &FileDialog,\n    preview_cache: &mut PreviewCache,\n    src_args: Option<&mut SourceArgs>,\n) {\n    ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);\n    if let Some(highlight) = dia.selected_entry() {\n        if let Some(parent) = highlight.as_path().parent() {\n            ui.label(egui::RichText::new(parent.display().to_string()).small());\n        }\n        if let Some(filename) = highlight.as_path().file_name() {\n            ui.label(filename.to_string_lossy());\n        }\n        ui.separator();\n        let ent_info = preview_cache.get_or_compute(highlight.as_path(), |path| EntInfo {\n            meta: std::fs::metadata(path),\n            mime: tree_magic_mini::from_filepath(path),\n        });\n        if let Some(mime) = ent_info.mime {\n            ui.label(mime);\n        }\n        match &ent_info.meta {\n            Ok(meta) => {\n                let ft = meta.file_type();\n                if ft.is_file() {\n                    ui.label(format!(\"Size: {}\", human_size_u64(meta.len())));\n                }\n                if ft.is_symlink() {\n                    ui.label(\"Symbolic link\");\n                }\n                if !(ft.is_file() || ft.is_dir()) {\n                    ui.label(format!(\"Special (size: {})\", meta.len()));\n                }\n            }\n            Err(e) => {\n                ui.label(e.to_string());\n            }\n        }\n        ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n        if ui.button(\"📋 Copy path to clipboard\").clicked() {\n            ui.ctx().copy_text(highlight.as_path().display().to_string());\n        }\n    } else {\n        ui.heading(\"Hexerator\");\n    }\n    ui.separator();\n    if let Some(src_args) = src_args {\n        src_args_ui(ui, src_args);\n    }\n}\n\nfn src_args_ui(ui: &mut egui::Ui, src_args: &mut SourceArgs) {\n    opt(\n        ui,\n        &mut src_args.jump,\n        \"jump\",\n        \"Jump to offset on startup\",\n        |ui, jump| {\n            ui.add(egui::DragValue::new(jump));\n        },\n    );\n    opt(\n        ui,\n        &mut src_args.hard_seek,\n        \"hard seek\",\n        \"Seek to offset, consider it beginning of the file in the editor\",\n        |ui, hard_seek| {\n            ui.add(egui::DragValue::new(hard_seek));\n        },\n    );\n    opt(\n        ui,\n        &mut src_args.take,\n        \"take\",\n        \"Read only this many bytes\",\n        |ui, take| {\n            ui.add(egui::DragValue::new(take));\n        },\n    );\n    ui.checkbox(&mut src_args.read_only, \"read-only\")\n        .on_hover_text(\"Open file as read-only\");\n    if ui\n        .checkbox(&mut src_args.stream, \"stream\")\n        .on_hover_text(\n            \"Specify source as a streaming source (for example, standard streams).\\n\\\n             Sets read-only attribute\",\n        )\n        .changed()\n    {\n        src_args.read_only = src_args.stream;\n    }\n    ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n    opt(\n        ui,\n        &mut src_args.unsafe_mmap,\n        \"⚠ mmap\",\n        MMAP_LABEL,\n        |ui, mode| {\n            let label = <&'static str>::from(&*mode);\n            egui::ComboBox::new(\"mmap_cbox\", \"mode\").selected_text(label).show_ui(ui, |ui| {\n                for variant in MmapMode::iter() {\n                    let label = <&'static str>::from(&variant);\n                    ui.selectable_value(mode, variant, label);\n                }\n            });\n        },\n    );\n    if src_args.unsafe_mmap == Some(MmapMode::DangerousMut) {\n        ui.label(DANGEROUS_MUT_LABEL);\n    }\n}\n\nconst MMAP_LABEL: &str = \"Open as memory mapped file\\n\\\n\\n\\\nWARNING\n\\n\\\nMemory mapped i/o is inherently unsafe.\nTo ensure no undefined behavior, make sure you have exclusive access to the file.\nThere is no warranty for any damage you might cause to your system.\n\";\n\nconst DANGEROUS_MUT_LABEL: &str = \"⚠ WARNING ⚠\\n\\\n\\n\\\nFile will be opened with a direct mutable memory map.\nAny changes made to the file will be IMMEDIATE.\nTHERE IS NO WAY TO UNDO ANY CHANGES.\n\";\n\nfn opt<V: Default>(\n    ui: &mut egui::Ui,\n    val: &mut Option<V>,\n    label: &str,\n    desc: &str,\n    f: impl FnOnce(&mut egui::Ui, &mut V),\n) {\n    ui.horizontal(|ui| {\n        let mut checked = val.is_some();\n        ui.checkbox(&mut checked, label).on_hover_text(desc);\n        if checked {\n            f(ui, val.get_or_insert_with(Default::default));\n        } else {\n            *val = None;\n        }\n    });\n}\n"
  },
  {
    "path": "src/gui/inspect_panel.rs",
    "content": "use {\n    super::message_dialog::{Icon, MessageDialog},\n    crate::{\n        app::{App, interact_mode::InteractMode},\n        damage_region::DamageRegion,\n        result_ext::AnyhowConv as _,\n        shell::msg_if_fail,\n        view::ViewportVec,\n    },\n    anyhow::bail,\n    egui::Ui,\n    slotmap::Key as _,\n    std::{array::TryFromSliceError, marker::PhantomData},\n    thiserror::Error,\n};\n\n#[derive(Debug, PartialEq, Eq, Clone, Copy)]\nenum Format {\n    Decimal,\n    Hex,\n    Bin,\n}\n\nimpl Format {\n    fn label(&self) -> &'static str {\n        match self {\n            Self::Decimal => \"Decimal\",\n            Self::Hex => \"Hex\",\n            Self::Bin => \"Binary\",\n        }\n    }\n}\n\npub struct InspectPanel {\n    input_thingies: [Box<dyn InputThingyTrait>; 11],\n    /// True if an input thingy was changed by the user. Should update the others\n    changed_one: bool,\n    big_endian: bool,\n    format: Format,\n    seek_relativity: SeekRelativity,\n    /// Edit buffer for user value for seek relative offset\n    seek_user_buf: String,\n    /// Computed user offset for seek relative offset\n    seek_user_offs: usize,\n    /// The value of the cursor on the previous frame. Used to determine when the cursor changes\n    pub prev_frame_inspect_offset: usize,\n}\n\n/// Relativity of seeking to an offset\n#[derive(Clone, Copy, PartialEq)]\nenum SeekRelativity {\n    /// Absolute offset in the file\n    Absolute,\n    /// Relative to hard-seek\n    HardSeek,\n    /// Relative to a user-defined offset\n    User,\n}\nimpl SeekRelativity {\n    fn label(&self) -> &'static str {\n        match self {\n            Self::Absolute => \"Absolute\",\n            Self::HardSeek => \"Hard seek\",\n            Self::User => \"User\",\n        }\n    }\n}\n\nimpl std::fmt::Debug for InspectPanel {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        f.debug_struct(\"InspectPanel\").finish()\n    }\n}\n\nimpl Default for InspectPanel {\n    fn default() -> Self {\n        Self {\n            input_thingies: [\n                Box::<InputThingy<i8>>::default(),\n                Box::<InputThingy<u8>>::default(),\n                Box::<InputThingy<i16>>::default(),\n                Box::<InputThingy<u16>>::default(),\n                Box::<InputThingy<i32>>::default(),\n                Box::<InputThingy<u32>>::default(),\n                Box::<InputThingy<i64>>::default(),\n                Box::<InputThingy<u64>>::default(),\n                Box::<InputThingy<f32>>::default(),\n                Box::<InputThingy<f64>>::default(),\n                Box::<InputThingy<Ascii>>::default(),\n            ],\n            changed_one: false,\n            big_endian: false,\n            format: Format::Decimal,\n            seek_relativity: SeekRelativity::Absolute,\n            prev_frame_inspect_offset: 0,\n            seek_user_buf: String::new(),\n            seek_user_offs: 0,\n        }\n    }\n}\n\ntrait InputThingyTrait {\n    fn update(&mut self, data: &[u8], offset: usize, be: bool, format: Format);\n    fn label(&self) -> &'static str;\n    fn buf_mut(&mut self) -> &mut String;\n    fn write_data(\n        &self,\n        data: &mut [u8],\n        offset: usize,\n        be: bool,\n        format: Format,\n        msg: &mut MessageDialog,\n    ) -> Option<DamageRegion>;\n}\n\nimpl<T: BytesManip> InputThingyTrait for InputThingy<T> {\n    fn update(&mut self, data: &[u8], offset: usize, be: bool, format: Format) {\n        T::update_buf(&mut self.string, data, offset, be, format);\n    }\n    fn label(&self) -> &'static str {\n        T::label()\n    }\n\n    fn buf_mut(&mut self) -> &mut String {\n        &mut self.string\n    }\n\n    fn write_data(\n        &self,\n        data: &mut [u8],\n        offset: usize,\n        be: bool,\n        format: Format,\n        msg: &mut MessageDialog,\n    ) -> Option<DamageRegion> {\n        T::convert_and_write(&self.string, data, offset, be, format, msg)\n    }\n}\n\n#[derive(Error, Debug)]\nenum FromBytesError {\n    #[error(\"Error converting from slice\")]\n    TryFromSlice(#[from] TryFromSliceError),\n    #[error(\"Error indexing slice\")]\n    SliceIndexError,\n}\n\ntrait NumBytesManip: std::fmt::Display + Sized {\n    type ToBytes: AsRef<[u8]>;\n    fn label() -> &'static str;\n    fn from_le_bytes(bytes: &[u8]) -> Result<Self, FromBytesError>;\n    fn from_be_bytes(bytes: &[u8]) -> Result<Self, FromBytesError>;\n    fn to_le_bytes(&self) -> Self::ToBytes;\n    fn to_be_bytes(&self) -> Self::ToBytes;\n    fn to_hex_string(&self) -> String;\n    fn to_bin_string(&self) -> String;\n    fn from_str(input: &str, format: Format) -> Result<Self, anyhow::Error>;\n}\n\nmacro_rules! num_bytes_manip_impl {\n    ($t:ty) => {\n        impl NumBytesManip for $t {\n            type ToBytes = [u8; <$t>::BITS as usize / 8];\n\n            fn label() -> &'static str {\n                stringify!($t)\n            }\n\n            fn from_le_bytes(bytes: &[u8]) -> Result<Self, FromBytesError> {\n                match bytes.get(..<$t>::BITS as usize / 8) {\n                    Some(slice) => Ok(Self::from_le_bytes(slice.try_into()?)),\n                    None => Err(FromBytesError::SliceIndexError),\n                }\n            }\n\n            fn from_be_bytes(bytes: &[u8]) -> Result<Self, FromBytesError> {\n                match bytes.get(..<$t>::BITS as usize / 8) {\n                    Some(slice) => Ok(Self::from_be_bytes(slice.try_into()?)),\n                    None => Err(FromBytesError::SliceIndexError),\n                }\n            }\n\n            fn to_le_bytes(&self) -> Self::ToBytes {\n                <$t>::to_le_bytes(*self)\n            }\n\n            fn to_be_bytes(&self) -> Self::ToBytes {\n                <$t>::to_be_bytes(*self)\n            }\n\n            fn to_hex_string(&self) -> String {\n                format!(\"{:x}\", self)\n            }\n\n            fn to_bin_string(&self) -> String {\n                format!(\"{:0w$b}\", self, w = <$t>::BITS as usize)\n            }\n\n            fn from_str(input: &str, format: Format) -> Result<Self, anyhow::Error> {\n                let this = match format {\n                    Format::Decimal => input.parse()?,\n                    Format::Hex => Self::from_str_radix(input, 16)?,\n                    Format::Bin => Self::from_str_radix(input, 2)?,\n                };\n                Ok(this)\n            }\n        }\n    };\n}\n\nnum_bytes_manip_impl!(i8);\nnum_bytes_manip_impl!(u8);\nnum_bytes_manip_impl!(i16);\nnum_bytes_manip_impl!(u16);\nnum_bytes_manip_impl!(i32);\nnum_bytes_manip_impl!(u32);\nnum_bytes_manip_impl!(i64);\nnum_bytes_manip_impl!(u64);\n\nimpl NumBytesManip for f32 {\n    type ToBytes = [u8; 32 / 8];\n\n    fn label() -> &'static str {\n        \"f32\"\n    }\n\n    fn from_le_bytes(bytes: &[u8]) -> Result<Self, FromBytesError> {\n        match bytes.get(..32 / 8) {\n            Some(slice) => Ok(Self::from_le_bytes(slice.try_into()?)),\n            None => Err(FromBytesError::SliceIndexError),\n        }\n    }\n\n    fn from_be_bytes(bytes: &[u8]) -> Result<Self, FromBytesError> {\n        match bytes.get(..32 / 8) {\n            Some(slice) => Ok(Self::from_be_bytes(slice.try_into()?)),\n            None => Err(FromBytesError::SliceIndexError),\n        }\n    }\n\n    fn to_le_bytes(&self) -> Self::ToBytes {\n        Self::to_le_bytes(*self)\n    }\n\n    fn to_be_bytes(&self) -> Self::ToBytes {\n        Self::to_be_bytes(*self)\n    }\n\n    fn to_hex_string(&self) -> String {\n        \"<no hex output>\".into()\n    }\n\n    fn to_bin_string(&self) -> String {\n        \"<no bin output>\".into()\n    }\n\n    fn from_str(input: &str, format: Format) -> Result<Self, anyhow::Error> {\n        let this = match format {\n            Format::Decimal => input.parse()?,\n            Format::Hex => bail!(\"Float doesn't support parsing hex\"),\n            Format::Bin => bail!(\"Float doesn't support parsing bin\"),\n        };\n        Ok(this)\n    }\n}\n\nimpl NumBytesManip for f64 {\n    type ToBytes = [u8; 8];\n\n    fn label() -> &'static str {\n        \"f64\"\n    }\n\n    fn from_le_bytes(bytes: &[u8]) -> Result<Self, FromBytesError> {\n        match bytes.get(..8) {\n            Some(slice) => Ok(Self::from_le_bytes(slice.try_into()?)),\n            None => Err(FromBytesError::SliceIndexError),\n        }\n    }\n\n    fn from_be_bytes(bytes: &[u8]) -> Result<Self, FromBytesError> {\n        match bytes.get(..8) {\n            Some(slice) => Ok(Self::from_be_bytes(slice.try_into()?)),\n            None => Err(FromBytesError::SliceIndexError),\n        }\n    }\n\n    fn to_le_bytes(&self) -> Self::ToBytes {\n        Self::to_le_bytes(*self)\n    }\n\n    fn to_be_bytes(&self) -> Self::ToBytes {\n        Self::to_le_bytes(*self)\n    }\n\n    fn to_hex_string(&self) -> String {\n        \"<no hex output>\".into()\n    }\n\n    fn to_bin_string(&self) -> String {\n        \"<no bin output>\".into()\n    }\n\n    fn from_str(input: &str, format: Format) -> Result<Self, anyhow::Error> {\n        let this = match format {\n            Format::Decimal => input.parse()?,\n            Format::Hex => bail!(\"Float doesn't support parsing hex\"),\n            Format::Bin => bail!(\"Float doesn't support parsing bin\"),\n        };\n        Ok(this)\n    }\n}\n\nimpl<T: NumBytesManip> BytesManip for T {\n    fn update_buf(buf: &mut String, data: &[u8], offset: usize, be: bool, format: Format) {\n        if let Some(slice) = &data.get(offset..) {\n            let result = if be {\n                T::from_be_bytes(slice)\n            } else {\n                T::from_le_bytes(slice)\n            };\n            *buf = match result {\n                Ok(value) => match format {\n                    Format::Decimal => value.to_string(),\n                    Format::Hex => value.to_hex_string(),\n                    Format::Bin => value.to_bin_string(),\n                },\n                Err(e) => e.to_string(),\n            }\n        }\n    }\n\n    fn label() -> &'static str {\n        <Self as NumBytesManip>::label()\n    }\n\n    fn convert_and_write(\n        buf: &str,\n        data: &mut [u8],\n        offset: usize,\n        be: bool,\n        format: Format,\n        msg: &mut MessageDialog,\n    ) -> Option<DamageRegion> {\n        match Self::from_str(buf, format) {\n            Ok(this) => {\n                let bytes = if be {\n                    this.to_be_bytes()\n                } else {\n                    this.to_le_bytes()\n                };\n                let range = offset..offset + bytes.as_ref().len();\n                match data.get_mut(range.clone()) {\n                    Some(slice) => {\n                        slice.copy_from_slice(bytes.as_ref());\n                        Some(DamageRegion::Range(range))\n                    }\n                    None => None,\n                }\n            }\n            Err(e) => {\n                msg.open(Icon::Error, \"Convert error\", e.to_string());\n                None\n            }\n        }\n    }\n}\n\nimpl BytesManip for Ascii {\n    fn update_buf(buf: &mut String, data: &[u8], offset: usize, _be: bool, _format: Format) {\n        if let Some(slice) = &data.get(offset..) {\n            let valid_ascii_end = find_valid_ascii_end(slice);\n            match String::from_utf8(data[offset..offset + valid_ascii_end].to_vec()) {\n                Ok(ascii) => *buf = ascii,\n                Err(e) => *buf = format!(\"[ascii error]: {e}\"),\n            }\n        }\n    }\n\n    fn label() -> &'static str {\n        \"ascii\"\n    }\n\n    fn convert_and_write(\n        buf: &str,\n        data: &mut [u8],\n        offset: usize,\n        _be: bool,\n        _format: Format,\n        msg: &mut MessageDialog,\n    ) -> Option<DamageRegion> {\n        let len = buf.len();\n        let range = offset..offset + len;\n        match data.get_mut(range.clone()) {\n            Some(slice) => {\n                slice.copy_from_slice(buf.as_bytes());\n                Some(DamageRegion::Range(range))\n            }\n            None => {\n                msg.open(\n                    Icon::Error,\n                    \"Convert and write error\",\n                    \"Failed to write data: Out of bounds\",\n                );\n                None\n            }\n        }\n    }\n}\n\nstruct InputThingy<T> {\n    string: String,\n    _phantom: PhantomData<T>,\n}\n\nimpl<T> Default for InputThingy<T> {\n    fn default() -> Self {\n        Self {\n            string: Default::default(),\n            _phantom: Default::default(),\n        }\n    }\n}\n\ntrait BytesManip {\n    fn update_buf(buf: &mut String, data: &[u8], offset: usize, be: bool, format: Format);\n    fn label() -> &'static str;\n    fn convert_and_write(\n        buf: &str,\n        data: &mut [u8],\n        offset: usize,\n        be: bool,\n        format: Format,\n        msg: &mut MessageDialog,\n    ) -> Option<DamageRegion>;\n}\n\nstruct Ascii;\n\nenum Action {\n    GoToOffset(usize),\n    AddDirty(DamageRegion),\n    JumpForward(usize),\n}\n\npub fn ui(ui: &mut Ui, app: &mut App, gui: &mut crate::gui::Gui, mouse_pos: ViewportVec) {\n    if app.hex_ui.current_layout.is_null() {\n        ui.label(\"No active layout\");\n        return;\n    }\n    let offset = match app.hex_ui.interact_mode {\n        InteractMode::View if !ui.egui_wants_pointer_input() => {\n            if let Some((off, _view_idx)) = app.byte_offset_at_pos(mouse_pos.x, mouse_pos.y) {\n                let mut add = 0;\n                match gui.inspect_panel.seek_relativity {\n                    SeekRelativity::Absolute => {}\n                    SeekRelativity::HardSeek => {\n                        add = app.src_args.hard_seek.unwrap_or(0);\n                    }\n                    SeekRelativity::User => {\n                        add = gui.inspect_panel.seek_user_offs;\n                    }\n                }\n                ui.link(format!(\"offset: {} (0x{:x})\", off + add, off + add))\n                    .context_menu(|ui| {\n                        if ui.button(\"Copy to clipboard\").clicked() {\n                            crate::app::set_clipboard_string(\n                                &mut app.clipboard,\n                                &mut gui.msg_dialog,\n                                &format!(\"{:x}\", off + add),\n                            );\n                        }\n                    });\n                off\n            } else {\n                edit_offset(app, gui, ui)\n            }\n        }\n        _ => edit_offset(app, gui, ui),\n    };\n    egui::ComboBox::new(\"seek_rela_cb\", \"Seek relativity\")\n        .selected_text(gui.inspect_panel.seek_relativity.label())\n        .show_ui(ui, |ui| {\n            ui.selectable_value(\n                &mut gui.inspect_panel.seek_relativity,\n                SeekRelativity::Absolute,\n                SeekRelativity::Absolute.label(),\n            );\n            ui.selectable_value(\n                &mut gui.inspect_panel.seek_relativity,\n                SeekRelativity::HardSeek,\n                SeekRelativity::HardSeek.label(),\n            );\n            ui.selectable_value(\n                &mut gui.inspect_panel.seek_relativity,\n                SeekRelativity::User,\n                SeekRelativity::User.label(),\n            );\n        });\n    let re = ui.add_enabled(\n        gui.inspect_panel.seek_relativity == SeekRelativity::User,\n        egui::TextEdit::singleline(&mut gui.inspect_panel.seek_user_buf),\n    );\n    if re.changed()\n        && let Ok(num) = gui.inspect_panel.seek_user_buf.parse()\n    {\n        gui.inspect_panel.seek_user_offs = num;\n    }\n    if app.data.is_empty() {\n        return;\n    }\n    for thingy in &mut gui.inspect_panel.input_thingies {\n        thingy.update(\n            &app.data[..],\n            offset,\n            gui.inspect_panel.big_endian,\n            gui.inspect_panel.format,\n        );\n    }\n    gui.inspect_panel.changed_one = false;\n    let mut actions = Vec::new();\n    for thingy in &mut gui.inspect_panel.input_thingies {\n        ui.horizontal(|ui| {\n            ui.label(thingy.label());\n            if ui.button(\"📋\").on_hover_text(\"copy to clipboard\").clicked() {\n                crate::app::set_clipboard_string(\n                    &mut app.clipboard,\n                    &mut gui.msg_dialog,\n                    thingy.buf_mut(),\n                );\n            }\n            if ui.button(\"⬇\").on_hover_text(\"go to offset\").clicked() {\n                let result = try {\n                    let offset = match gui.inspect_panel.format {\n                        Format::Decimal => thingy.buf_mut().parse().how()?,\n                        Format::Hex => usize::from_str_radix(thingy.buf_mut(), 16).how()?,\n                        Format::Bin => usize::from_str_radix(thingy.buf_mut(), 2).how()?,\n                    };\n                    actions.push(Action::GoToOffset(offset));\n                };\n                msg_if_fail(result, \"Failed to go to offset\", &mut gui.msg_dialog);\n            }\n            if ui.button(\"➡\").on_hover_text(\"jump forward\").clicked() {\n                let result = try {\n                    let offset = match gui.inspect_panel.format {\n                        Format::Decimal => thingy.buf_mut().parse().how()?,\n                        Format::Hex => usize::from_str_radix(thingy.buf_mut(), 16).how()?,\n                        Format::Bin => usize::from_str_radix(thingy.buf_mut(), 2).how()?,\n                    };\n                    actions.push(Action::JumpForward(offset));\n                };\n                msg_if_fail(result, \"Failed to jump forward\", &mut gui.msg_dialog);\n            }\n        });\n        if ui.text_edit_singleline(thingy.buf_mut()).lost_focus()\n            && ui.input(|inp| inp.key_pressed(egui::Key::Enter))\n            && let Some(range) = thingy.write_data(\n                &mut app.data,\n                offset,\n                gui.inspect_panel.big_endian,\n                gui.inspect_panel.format,\n                &mut gui.msg_dialog,\n            )\n        {\n            gui.inspect_panel.changed_one = true;\n            actions.push(Action::AddDirty(range));\n        }\n    }\n    ui.horizontal(|ui| {\n        if ui.checkbox(&mut gui.inspect_panel.big_endian, \"Big endian\").clicked() {\n            // Changing this should refresh everything\n            gui.inspect_panel.changed_one = true;\n        }\n        let prev_fmt = gui.inspect_panel.format;\n        egui::ComboBox::new(\"format_combo\", \"format\")\n            .selected_text(gui.inspect_panel.format.label())\n            .show_ui(ui, |ui| {\n                ui.selectable_value(\n                    &mut gui.inspect_panel.format,\n                    Format::Decimal,\n                    Format::Decimal.label(),\n                );\n                ui.selectable_value(\n                    &mut gui.inspect_panel.format,\n                    Format::Hex,\n                    Format::Hex.label(),\n                );\n                ui.selectable_value(\n                    &mut gui.inspect_panel.format,\n                    Format::Bin,\n                    Format::Bin.label(),\n                );\n            });\n\n        if gui.inspect_panel.format != prev_fmt {\n            // Changing the format should refresh everything\n            gui.inspect_panel.changed_one = true;\n        }\n    });\n\n    for action in actions {\n        match action {\n            Action::GoToOffset(offset) => {\n                match gui.inspect_panel.seek_relativity {\n                    SeekRelativity::Absolute => {\n                        app.edit_state.set_cursor(offset);\n                    }\n                    SeekRelativity::HardSeek => {\n                        app.edit_state.set_cursor(offset - app.src_args.hard_seek.unwrap_or(0));\n                    }\n                    SeekRelativity::User => {\n                        app.edit_state.set_cursor(offset - gui.inspect_panel.seek_user_offs);\n                    }\n                }\n                app.center_view_on_offset(app.edit_state.cursor);\n                app.hex_ui.flash_cursor();\n            }\n            Action::AddDirty(damage) => app.data.widen_dirty_region(damage),\n            Action::JumpForward(amount) => {\n                app.edit_state.set_cursor(app.edit_state.cursor + amount);\n                app.center_view_on_offset(app.edit_state.cursor);\n                app.hex_ui.flash_cursor();\n            }\n        }\n    }\n    gui.inspect_panel.prev_frame_inspect_offset = offset;\n}\n\nfn edit_offset(app: &mut App, gui: &mut crate::gui::Gui, ui: &mut Ui) -> usize {\n    let mut off = app.edit_state.cursor;\n    match gui.inspect_panel.seek_relativity {\n        SeekRelativity::Absolute => {}\n        SeekRelativity::HardSeek => {\n            off += app.src_args.hard_seek.unwrap_or(0);\n        }\n        SeekRelativity::User => {\n            off += gui.inspect_panel.seek_user_offs;\n        }\n    }\n    ui.link(format!(\"offset: {off} ({off:x}h)\")).context_menu(|ui| {\n        if ui.button(\"Copy to clipboard\").clicked() {\n            crate::app::set_clipboard_string(\n                &mut app.clipboard,\n                &mut gui.msg_dialog,\n                &format!(\"{off:x}\"),\n            );\n        }\n    });\n    app.edit_state.cursor\n}\n\nfn find_valid_ascii_end(data: &[u8]) -> usize {\n    // Don't try to take too many characters, as that degrades performance\n    const MAX_TAKE: usize = 50;\n    data.iter()\n        .take(MAX_TAKE)\n        .position(|&b| b == 0 || b > 127)\n        .unwrap_or_else(|| std::cmp::min(MAX_TAKE, data.len()))\n}\n"
  },
  {
    "path": "src/gui/message_dialog.rs",
    "content": "use {\n    crate::app::command::CommandQueue,\n    core::f32,\n    egui::Color32,\n    std::{backtrace::Backtrace, collections::VecDeque},\n};\n\n#[derive(Default)]\npub struct MessageDialog {\n    payloads: VecDeque<Payload>,\n}\n\npub struct Payload {\n    pub title: String,\n    pub desc: String,\n    pub icon: Icon,\n    pub buttons_ui_fn: Option<Box<UiFn>>,\n    pub backtrace: Option<Backtrace>,\n    pub show_backtrace: bool,\n    pub close: bool,\n}\n\n#[derive(Default)]\npub enum Icon {\n    #[default]\n    None,\n    Info,\n    Warn,\n    Error,\n}\n\npub(crate) type UiFn = dyn FnMut(&mut egui::Ui, &mut Payload, &mut CommandQueue);\n\n// Colors and icon text are copied from egui-toast, for visual consistency\n// https://github.com/urholaukkarinen/egui-toast\nimpl Icon {\n    fn color(&self) -> Color32 {\n        match self {\n            Self::None => Color32::default(),\n            Self::Info => Color32::from_rgb(0, 155, 255),\n            Self::Warn => Color32::from_rgb(255, 212, 0),\n            Self::Error => Color32::from_rgb(255, 32, 0),\n        }\n    }\n    fn utf8(&self) -> &'static str {\n        match self {\n            Self::None => \"\",\n            Self::Info => \"ℹ\",\n            Self::Warn => \"⚠\",\n            Self::Error => \"❗\",\n        }\n    }\n    fn hover_text(&self) -> String {\n        let label = match self {\n            Self::None => \"\",\n            Self::Info => \"Info\",\n            Self::Warn => \"Warning\",\n            Self::Error => \"Error\",\n        };\n        format!(\"{label}\\n\\nClick to copy message to clipboard\")\n    }\n    fn is_set(&self) -> bool {\n        !matches!(self, Self::None)\n    }\n}\n\nimpl MessageDialog {\n    pub(crate) fn open(&mut self, icon: Icon, title: impl Into<String>, desc: impl Into<String>) {\n        self.payloads.push_back(Payload {\n            title: title.into(),\n            desc: desc.into(),\n            icon,\n            buttons_ui_fn: None,\n            backtrace: None,\n            show_backtrace: false,\n            close: false,\n        });\n    }\n    pub(crate) fn custom_button_row_ui(&mut self, f: Box<UiFn>) {\n        if let Some(front) = self.payloads.front_mut() {\n            front.buttons_ui_fn = Some(f);\n        }\n    }\n    pub(crate) fn show(\n        &mut self,\n        ctx: &egui::Context,\n        cb: &mut arboard::Clipboard,\n        cmd: &mut CommandQueue,\n    ) {\n        let payloads_len = self.payloads.len();\n        let Some(payload) = self.payloads.front_mut() else {\n            return;\n        };\n        let mut close = false;\n        egui::Modal::new(\"msg_dialog_popup\".into()).show(ctx, |ui| {\n            ui.horizontal(|ui| {\n                ui.heading(&payload.title);\n                if payloads_len > 1 {\n                    ui.label(format!(\"({} more)\", payloads_len - 1));\n                }\n            });\n            ui.vertical_centered_justified(|ui| {\n                ui.horizontal(|ui| {\n                    if payload.icon.is_set()\n                        && ui\n                            .add(\n                                egui::Label::new(\n                                    egui::RichText::new(payload.icon.utf8())\n                                        .color(payload.icon.color())\n                                        .size(32.0),\n                                )\n                                .sense(egui::Sense::click()),\n                            )\n                            .on_hover_text(payload.icon.hover_text())\n                            .clicked()\n                        && let Err(e) = cb.set_text(payload.desc.clone())\n                    {\n                        gamedebug_core::per!(\"Clipboard set error: {e:?}\");\n                    }\n                    ui.label(&payload.desc);\n                });\n                if let Some(bt) = &payload.backtrace {\n                    ui.with_layout(egui::Layout::top_down(egui::Align::Min), |ui| {\n                        ui.checkbox(&mut payload.show_backtrace, \"Show backtrace\");\n                        if payload.show_backtrace {\n                            let bt = bt.to_string();\n                            egui::ScrollArea::both().max_height(300.0).show(ui, |ui| {\n                                ui.add(\n                                    egui::TextEdit::multiline(&mut bt.as_str())\n                                        .code_editor()\n                                        .desired_width(f32::INFINITY),\n                                );\n                            });\n                        }\n                    });\n                }\n                let (enter_pressed, esc_pressed) = ui.input_mut(|inp| {\n                    (\n                        // Consume enter and escape, so when the dialog is closed\n                        // using these keys, the normal UI won't receive these keys right away.\n                        // Receiving the keys could for example cause a text parse box\n                        // that parses on enter press to parse again right away with the\n                        // same error when the message box is closed with enter.\n                        inp.consume_key(egui::Modifiers::default(), egui::Key::Enter),\n                        inp.consume_key(egui::Modifiers::default(), egui::Key::Escape),\n                    )\n                });\n                let mut buttons_ui_fn = payload.buttons_ui_fn.take();\n                match &mut buttons_ui_fn {\n                    Some(f) => f(ui, payload, cmd),\n                    None => {\n                        if ui.button(\"Ok\").clicked() || enter_pressed || esc_pressed {\n                            payload.backtrace = None;\n                            close = true;\n                        }\n                    }\n                }\n                payload.buttons_ui_fn = buttons_ui_fn;\n            });\n        });\n        if close || payload.close {\n            self.payloads.pop_front();\n        }\n    }\n    pub fn set_backtrace_for_top(&mut self, bt: Backtrace) {\n        if let Some(front) = self.payloads.front_mut() {\n            front.backtrace = Some(bt);\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/ops.rs",
    "content": "//! Various common operations that are triggered by gui interactions\n\nuse crate::{gui::windows::RegionsWindow, meta::region::Region, meta_state::MetaState};\n\npub fn add_region_from_selection(\n    selection: Region,\n    app_meta_state: &mut MetaState,\n    gui_regions_window: &mut RegionsWindow,\n) {\n    let key = app_meta_state.meta.add_region_from_selection(selection);\n    gui_regions_window.open.set(true);\n    gui_regions_window.selected_key = Some(key);\n    gui_regions_window.activate_rename = true;\n}\n"
  },
  {
    "path": "src/gui/root_ctx_menu.rs",
    "content": "use {\n    super::Gui,\n    crate::{app::App, meta::ViewKey, view::ViewportScalar},\n    constcat::concat,\n    egui_phosphor::regular as ic,\n};\n\nconst L_SELECTION: &str = concat!(ic::SELECTION, \" Selection\");\nconst L_REGION_PROPS: &str = concat!(ic::RULER, \" Region properties...\");\nconst L_VIEW_PROPS: &str = concat!(ic::EYE, \" View properties...\");\nconst L_CHANGE_THIS_VIEW: &str = concat!(ic::SWAP, \" Change this view to\");\nconst L_REMOVE_FROM_LAYOUT: &str = concat!(ic::TRASH, \" Remove from layout\");\nconst L_OPEN_BOOKMARK: &str = concat!(ic::BOOKMARK, \" Open bookmark\");\nconst L_ADD_BOOKMARK: &str = concat!(ic::BOOKMARK, \" Add bookmark\");\nconst L_LAYOUT_PROPS: &str = concat!(ic::LAYOUT, \" Layout properties...\");\nconst L_LAYOUTS: &str = concat!(ic::LAYOUT, \" Layouts\");\n\npub struct ContextMenu {\n    pos: egui::Pos2,\n    data: ContextMenuData,\n}\n\nimpl ContextMenu {\n    pub fn new(mx: ViewportScalar, my: ViewportScalar, data: ContextMenuData) -> Self {\n        Self {\n            pos: egui::pos2(f32::from(mx), f32::from(my)),\n            data,\n        }\n    }\n}\n\npub struct ContextMenuData {\n    pub view: Option<ViewKey>,\n    pub byte_off: Option<usize>,\n}\n\n/// Yoinked from egui source code\nfn set_menu_style(style: &mut egui::Style) {\n    style.spacing.button_padding = egui::vec2(2.0, 0.0);\n    style.visuals.widgets.active.bg_stroke = egui::Stroke::NONE;\n    style.visuals.widgets.hovered.bg_stroke = egui::Stroke::NONE;\n    style.visuals.widgets.inactive.weak_bg_fill = egui::Color32::TRANSPARENT;\n    style.visuals.widgets.inactive.bg_stroke = egui::Stroke::NONE;\n    style.wrap_mode = Some(egui::TextWrapMode::Extend);\n}\n\n/// Returns whether to keep root context menu open\n#[must_use]\npub(super) fn show(menu: &ContextMenu, ctx: &egui::Context, app: &mut App, gui: &mut Gui) -> bool {\n    let mut close = false;\n    egui::Area::new(\"root_ctx_menu\".into())\n        .kind(egui::UiKind::Menu)\n        .order(egui::Order::Foreground)\n        .fixed_pos(menu.pos)\n        .default_width(ctx.global_style().spacing.menu_width)\n        .sense(egui::Sense::hover())\n        .show(ctx, |ui| {\n            set_menu_style(ui.style_mut());\n            egui::Frame::menu(ui.style()).show(ui, |ui| {\n                ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {\n                    menu_inner_ui(app, ui, gui, &mut close, menu);\n                });\n            });\n        });\n    !close\n}\n\nfn menu_inner_ui(\n    app: &mut App,\n    ui: &mut egui::Ui,\n    gui: &mut Gui,\n    close: &mut bool,\n    menu: &ContextMenu,\n) {\n    if let Some(sel) = app.hex_ui.selection() {\n        ui.separator();\n        if crate::gui::selection_menu::selection_menu(\n            L_SELECTION,\n            ui,\n            app,\n            &mut gui.dialogs,\n            &mut gui.msg_dialog,\n            &mut gui.win.regions,\n            sel,\n            &mut gui.fileops,\n        ) {\n            *close = true;\n        }\n    }\n    if let Some(view) = menu.data.view {\n        ui.separator();\n        if ui.button(L_REGION_PROPS).clicked() {\n            gui.win.regions.selected_key = Some(app.region_key_for_view(view));\n            gui.win.regions.open.set(true);\n            *close = true;\n        }\n        if ui.button(L_VIEW_PROPS).clicked() {\n            gui.win.views.selected = view;\n            gui.win.views.open.set(true);\n            *close = true;\n        }\n        ui.menu_button(L_CHANGE_THIS_VIEW, |ui| {\n            ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n            let Some(layout) = app.meta_state.meta.layouts.get_mut(app.hex_ui.current_layout)\n            else {\n                return;\n            };\n            for (k, v) in\n                app.meta_state.meta.views.iter().filter(|(k, _)| !layout.contains_view(*k))\n            {\n                if ui.button(&v.name).clicked() {\n                    layout.change_view_type(view, k);\n\n                    *close = true;\n                    return;\n                }\n            }\n        });\n        if ui.button(L_REMOVE_FROM_LAYOUT).clicked()\n            && let Some(layout) = app.meta_state.meta.layouts.get_mut(app.hex_ui.current_layout)\n        {\n            layout.remove_view(view);\n            if app.hex_ui.focused_view == Some(view) {\n                let first_view = layout.view_grid.first().and_then(|row| row.first());\n                app.hex_ui.focused_view = first_view.cloned();\n            }\n            *close = true;\n        }\n    }\n    if let Some(byte_off) = menu.data.byte_off {\n        ui.separator();\n        match app.meta_state.meta.bookmarks.iter().position(|bm| bm.offset == byte_off) {\n            Some(pos) => {\n                if ui.button(L_OPEN_BOOKMARK).clicked() {\n                    gui.win.bookmarks.open.set(true);\n                    gui.win.bookmarks.selected = Some(pos);\n                    *close = true;\n                }\n            }\n            None => {\n                if ui.button(L_ADD_BOOKMARK).clicked() {\n                    crate::gui::add_new_bookmark(app, gui, byte_off);\n                    *close = true;\n                }\n            }\n        }\n    }\n    ui.separator();\n    if ui.button(L_LAYOUT_PROPS).clicked() {\n        gui.win.layouts.open.toggle();\n        *close = true;\n    }\n    ui.menu_button(L_LAYOUTS, |ui| {\n        for (key, layout) in app.meta_state.meta.layouts.iter() {\n            if ui.button(&layout.name).clicked() {\n                App::switch_layout(&mut app.hex_ui, &app.meta_state.meta, key);\n\n                *close = true;\n            }\n        }\n    });\n}\n"
  },
  {
    "path": "src/gui/selection_menu.rs",
    "content": "use {\n    crate::{\n        app::App,\n        damage_region::DamageRegion,\n        gui::{\n            Gui,\n            dialogs::{LuaFillDialog, PatternFillDialog, X86AsmDialog},\n            file_ops::FileOps,\n            message_dialog::MessageDialog,\n            windows::RegionsWindow,\n        },\n        shell::msg_fail,\n    },\n    constcat::concat,\n    egui::Button,\n    egui_phosphor::regular as ic,\n    rand::Rng as _,\n    std::fmt::Write as _,\n};\n\nconst L_UNSELECT: &str = concat!(ic::SELECTION_SLASH, \" Unselect\");\nconst L_ZERO_FILL: &str = concat!(ic::NUMBER_SQUARE_ZERO, \" Zero fill\");\nconst L_PATTERN_FILL: &str = concat!(ic::BINARY, \" Pattern fill...\");\nconst L_LUA_FILL: &str = concat!(ic::MOON, \" Lua fill...\");\nconst L_RANDOM_FILL: &str = concat!(ic::SHUFFLE, \" Random fill\");\nconst L_COPY_AS_HEX_TEXT: &str = concat!(ic::COPY, \" Copy as hex text\");\nconst L_COPY_AS_UTF8: &str = concat!(ic::COPY, \" Copy as utf-8 text\");\nconst L_ADD_AS_REGION: &str = concat!(ic::RULER, \" Add as region\");\nconst L_SAVE_TO_FILE: &str = concat!(ic::FLOPPY_DISK, \" Save to file\");\nconst L_X86_ASM: &str = concat!(ic::PIPE_WRENCH, \" X86 asm\");\n\n/// Returns whether anything was clicked\npub fn selection_menu(\n    title: &str,\n    ui: &mut egui::Ui,\n    app: &mut App,\n    gui_dialogs: &mut crate::gui::Dialogs,\n    gui_msg_dialog: &mut MessageDialog,\n    gui_regions_window: &mut RegionsWindow,\n    sel: crate::meta::region::Region,\n    file_ops: &mut FileOps,\n) -> bool {\n    let mut clicked = false;\n    ui.menu_button(title, |ui| {\n        if ui.add(Button::new(L_UNSELECT).shortcut_text(\"Esc\")).clicked() {\n            app.hex_ui.clear_selections();\n\n            clicked = true;\n        }\n        if ui.add(Button::new(L_ZERO_FILL).shortcut_text(\"Del\")).clicked() {\n            app.data.zero_fill_region(sel);\n\n            clicked = true;\n        }\n        if ui.button(L_PATTERN_FILL).clicked() {\n            Gui::add_dialog(gui_dialogs, PatternFillDialog::default());\n\n            clicked = true;\n        }\n        if ui.button(L_LUA_FILL).clicked() {\n            Gui::add_dialog(gui_dialogs, LuaFillDialog::default());\n\n            clicked = true;\n        }\n        if ui.button(L_RANDOM_FILL).clicked() {\n            for region in app.hex_ui.selected_regions() {\n                if let Some(data) = app.data.get_mut(region.to_range()) {\n                    rand::rng().fill_bytes(data);\n                    app.data.widen_dirty_region(DamageRegion::RangeInclusive(region.to_range()));\n                }\n            }\n\n            clicked = true;\n        }\n        if ui.button(L_COPY_AS_HEX_TEXT).clicked() {\n            let mut s = String::new();\n            let result = try {\n                for &byte in &app.data[sel.begin..=sel.end] {\n                    write!(&mut s, \"{byte:02x} \")?;\n                }\n            };\n            match result {\n                Ok(()) => {\n                    crate::app::set_clipboard_string(\n                        &mut app.clipboard,\n                        gui_msg_dialog,\n                        s.trim_end(),\n                    );\n                }\n                Err(e) => {\n                    msg_fail(&e, \"Failed to copy as hex text\", gui_msg_dialog);\n                }\n            }\n\n            clicked = true;\n        }\n        if ui.button(L_COPY_AS_UTF8).clicked() {\n            let s = String::from_utf8_lossy(&app.data[sel.begin..=sel.end]);\n            crate::app::set_clipboard_string(&mut app.clipboard, gui_msg_dialog, &s);\n\n            clicked = true;\n        }\n        if ui.button(L_ADD_AS_REGION).clicked() {\n            crate::gui::ops::add_region_from_selection(\n                sel,\n                &mut app.meta_state,\n                gui_regions_window,\n            );\n\n            clicked = true;\n        }\n        if ui.button(L_SAVE_TO_FILE).clicked() {\n            file_ops.save_selection_to_file(sel);\n\n            clicked = true;\n        }\n        if ui.button(L_X86_ASM).clicked() {\n            Gui::add_dialog(gui_dialogs, X86AsmDialog::new());\n\n            clicked = true;\n        }\n    });\n    clicked\n}\n"
  },
  {
    "path": "src/gui/top_menu/analysis.rs",
    "content": "use {\n    crate::{\n        app::App,\n        gui::{Gui, message_dialog::Icon},\n        shell::msg_if_fail,\n    },\n    constcat::concat,\n    egui_phosphor::regular as ic,\n};\n\nconst L_DETERMINE_DATA_MIME: &str =\n    concat!(ic::SEAL_QUESTION, \" Determine data mime type under cursor\");\nconst L_DETERMINE_DATA_MIME_SEL: &str =\n    concat!(ic::SEAL_QUESTION, \" Determine data mime type of selection\");\nconst L_DIFF_WITH_FILE: &str = concat!(ic::GIT_DIFF, \" Diff with file...\");\nconst L_DIFF_WITH_SOURCE_FILE: &str = concat!(ic::GIT_DIFF, \" Diff with source file\");\nconst L_DIFF_WITH_BACKUP: &str = concat!(ic::GIT_DIFF, \" Diff with backup\");\nconst L_FIND_MEMORY_POINTERS: &str = concat!(ic::ARROW_UP_RIGHT, \" Find memory pointers...\");\nconst L_ZERO_PARTITION: &str = concat!(ic::BINARY, \" Zero partition...\");\n\npub fn ui(ui: &mut egui::Ui, gui: &mut Gui, app: &App) {\n    if ui.button(L_DETERMINE_DATA_MIME).clicked() {\n        gui.msg_dialog.open(\n            Icon::Info,\n            \"Data mime type under cursor\",\n            tree_magic_mini::from_u8(&app.data[app.edit_state.cursor..]).to_string(),\n        );\n    }\n    if let Some(region) = app.hex_ui.selection()\n        && ui.button(L_DETERMINE_DATA_MIME_SEL).clicked()\n    {\n        gui.msg_dialog.open(\n            Icon::Info,\n            \"Data mime type of selection\",\n            tree_magic_mini::from_u8(&app.data[region.begin..=region.end]).to_string(),\n        );\n    }\n    ui.separator();\n    if ui.button(L_DIFF_WITH_FILE).clicked() {\n        gui.fileops.diff_with_file(app.source_file());\n    }\n    if ui.button(L_DIFF_WITH_SOURCE_FILE).clicked()\n        && let Some(path) = app.source_file()\n    {\n        let path = path.to_owned();\n        msg_if_fail(\n            app.diff_with_file(path, &mut gui.win.file_diff_result),\n            \"Failed to diff\",\n            &mut gui.msg_dialog,\n        );\n    }\n    match app.backup_path() {\n        Some(path) if path.exists() => {\n            if ui.button(L_DIFF_WITH_BACKUP).clicked() {\n                msg_if_fail(\n                    app.diff_with_file(path, &mut gui.win.file_diff_result),\n                    \"Failed to diff\",\n                    &mut gui.msg_dialog,\n                );\n            }\n        }\n        _ => {\n            ui.add_enabled(false, egui::Button::new(L_DIFF_WITH_BACKUP));\n        }\n    }\n    ui.separator();\n    if ui\n        .add_enabled(\n            gui.win.open_process.selected_pid.is_some(),\n            egui::Button::new(L_FIND_MEMORY_POINTERS),\n        )\n        .on_disabled_hover_text(\"Requires open process\")\n        .clicked()\n    {\n        gui.win.find_memory_pointers.open.toggle();\n    }\n    if ui\n        .button(L_ZERO_PARTITION)\n        .on_hover_text(\"Find regions of non-zero data separated by zeroed regions\")\n        .clicked()\n    {\n        gui.win.zero_partition.open.toggle();\n    }\n}\n"
  },
  {
    "path": "src/gui/top_menu/cursor.rs",
    "content": "use {\n    crate::{\n        app::App,\n        gui::{Gui, dialogs::JumpDialog},\n    },\n    constcat::concat,\n    egui::Button,\n    egui_phosphor::regular as ic,\n};\n\nconst L_RESET: &str = concat!(ic::ARROW_U_UP_LEFT, \" Reset\");\nconst L_JUMP: &str = concat!(ic::SHARE_FAT, \" Jump...\");\nconst L_FLASH_CURSOR: &str = concat!(ic::LIGHTBULB, \" Flash cursor\");\nconst L_CENTER_VIEW_ON_CURSOR: &str = concat!(ic::CROSSHAIR, \" Center view on cursor\");\n\npub fn ui(ui: &mut egui::Ui, gui: &mut Gui, app: &mut App) {\n    let re = ui.button(L_RESET).on_hover_text(\n        \"Set to initial position.\\n\\\n                        This will be --jump argument, if one was provided, 0 otherwise\",\n    );\n    if re.clicked() {\n        app.set_cursor_init();\n    }\n    if ui.add(Button::new(L_JUMP).shortcut_text(\"Ctrl+J\")).clicked() {\n        Gui::add_dialog(&mut gui.dialogs, JumpDialog::default());\n    }\n    if ui.button(L_FLASH_CURSOR).clicked() {\n        app.preferences.hide_cursor = false;\n        app.hex_ui.flash_cursor();\n    }\n    if ui.button(L_CENTER_VIEW_ON_CURSOR).clicked() {\n        app.preferences.hide_cursor = false;\n        app.center_view_on_offset(app.edit_state.cursor);\n        app.hex_ui.flash_cursor();\n    }\n    ui.checkbox(&mut app.preferences.hide_cursor, \"Hide cursor\");\n}\n"
  },
  {
    "path": "src/gui/top_menu/edit.rs",
    "content": "use {\n    crate::{\n        app::{\n            App,\n            command::{Cmd, perform_command},\n        },\n        gui::{Gui, dialogs::TruncateDialog, message_dialog::Icon},\n        result_ext::AnyhowConv as _,\n        shell::msg_if_fail,\n    },\n    constcat::concat,\n    egui::Button,\n    egui_phosphor::regular as ic,\n    mlua::Lua,\n};\n\nconst L_FIND: &str = concat!(ic::MAGNIFYING_GLASS, \" Find...\");\nconst L_SELECTION: &str = concat!(ic::SELECTION, \" Selection\");\nconst L_SELECT_A: &str = \"🅰 Set select a\";\nconst L_SELECT_B: &str = \"🅱 Set select b\";\nconst L_SELECT_ALL: &str = concat!(ic::SELECTION_ALL, \" Select all in region\");\nconst L_SELECT_ROW: &str = concat!(ic::ARROWS_HORIZONTAL, \" Select row\");\nconst L_SELECT_COL: &str = concat!(ic::ARROWS_VERTICAL, \" Select column\");\nconst L_EXTERNAL_COMMAND: &str = concat!(ic::TERMINAL_WINDOW, \" External command...\");\nconst L_INC_BYTE: &str = concat!(ic::PLUS, \" Inc byte(s)\");\nconst L_DEC_BYTE: &str = concat!(ic::MINUS, \" Dec byte(s)\");\nconst L_PASTE_AT_CURSOR: &str = concat!(ic::CLIPBOARD_TEXT, \" Paste at cursor\");\nconst L_TRUNCATE_EXTEND: &str = concat!(ic::SCISSORS, \" Truncate/Extend...\");\n\npub fn ui(\n    ui: &mut egui::Ui,\n    gui: &mut Gui,\n    app: &mut App,\n    lua: &Lua,\n    font_size: u16,\n    line_spacing: u16,\n) {\n    if ui.add(Button::new(L_FIND).shortcut_text(\"Ctrl+F\")).clicked() {\n        gui.win.find.open.toggle();\n    }\n    ui.separator();\n    match app.hex_ui.selection() {\n        Some(sel) => {\n            if crate::gui::selection_menu::selection_menu(\n                L_SELECTION,\n                ui,\n                app,\n                &mut gui.dialogs,\n                &mut gui.msg_dialog,\n                &mut gui.win.regions,\n                sel,\n                &mut gui.fileops,\n            ) {}\n        }\n        None => {\n            ui.label(\"<No selection>\");\n        }\n    }\n    if ui.add(Button::new(L_SELECT_A).shortcut_text(\"shift+1\")).clicked() {\n        app.hex_ui.select_a = Some(app.edit_state.cursor);\n    }\n    if ui.add(Button::new(L_SELECT_B).shortcut_text(\"shift+2\")).clicked() {\n        app.hex_ui.select_b = Some(app.edit_state.cursor);\n    }\n    if ui.add(Button::new(L_SELECT_ALL).shortcut_text(\"Ctrl+A\")).clicked() {\n        app.focused_view_select_all();\n    }\n    if ui.add(Button::new(L_SELECT_ROW)).clicked() {\n        app.focused_view_select_row();\n    }\n    if ui.add(Button::new(L_SELECT_COL)).clicked() {\n        app.focused_view_select_col();\n    }\n    ui.separator();\n    if ui.add(Button::new(L_EXTERNAL_COMMAND).shortcut_text(\"Ctrl+E\")).clicked() {\n        gui.win.external_command.open.toggle();\n    }\n    ui.separator();\n    if ui\n        .add(Button::new(L_INC_BYTE).shortcut_text(\"Ctrl+=\"))\n        .on_hover_text(\"Increase byte(s) of selection or at cursor\")\n        .clicked()\n    {\n        app.inc_byte_or_bytes();\n    }\n    if ui\n        .add(Button::new(L_DEC_BYTE).shortcut_text(\"Ctrl+-\"))\n        .on_hover_text(\"Decrease byte(s) of selection or at cursor\")\n        .clicked()\n    {\n        app.dec_byte_or_bytes();\n    }\n    ui.menu_button(L_PASTE_AT_CURSOR, |ui| {\n        if ui.button(\"Hex text from clipboard\").clicked() {\n            let s = crate::app::get_clipboard_string(&mut app.clipboard, &mut gui.msg_dialog);\n            let cursor = app.edit_state.cursor;\n            let result = try {\n                let bytes = s\n                    .split_ascii_whitespace()\n                    .map(|s| u8::from_str_radix(s, 16))\n                    .collect::<Result<Vec<_>, _>>()\n                    .how()?;\n                if cursor + bytes.len() < app.data.len() {\n                    perform_command(\n                        app,\n                        Cmd::PasteBytes { at: cursor, bytes },\n                        gui,\n                        lua,\n                        font_size,\n                        line_spacing,\n                    );\n                } else {\n                    gui.msg_dialog.open(\n                        Icon::Warn,\n                        \"Prompt\",\n                        \"Paste overflows the document. What do do?\",\n                    );\n                    gui.msg_dialog.custom_button_row_ui(Box::new(move |ui, payload, cmd| {\n                        if ui.button(\"Cancel paste\").clicked() {\n                            payload.close = true;\n                        } else if ui.button(\"Extend document\").clicked() {\n                            cmd.push(Cmd::ExtendDocument {\n                                new_len: cursor + bytes.len(),\n                            });\n                            cmd.push(Cmd::PasteBytes {\n                                at: cursor,\n                                bytes: bytes.clone(),\n                            });\n                            payload.close = true;\n                        } else if ui.button(\"Shorten paste\").clicked() {\n                        }\n                    }));\n                }\n            };\n            msg_if_fail(result, \"Hex text paste error\", &mut gui.msg_dialog);\n        }\n    });\n    ui.separator();\n    ui.checkbox(&mut app.preferences.move_edit_cursor, \"Move edit cursor\")\n        .on_hover_text(\n            \"With the cursor keys in edit mode, move edit cursor by default.\\n\\\n                        Otherwise, block cursor is moved. Can use ctrl+cursor keys for\n                        the other behavior.\",\n        );\n    ui.checkbox(&mut app.preferences.quick_edit, \"Quick edit\").on_hover_text(\n        \"Immediately apply editing results, instead of having to type a \\\n                        value to completion or press enter\",\n    );\n    ui.checkbox(&mut app.preferences.sticky_edit, \"Sticky edit\")\n        .on_hover_text(\"Don't automatically move cursor after editing is finished\");\n    ui.separator();\n    if ui.button(L_TRUNCATE_EXTEND).clicked() {\n        Gui::add_dialog(\n            &mut gui.dialogs,\n            TruncateDialog::new(app.data.len(), app.hex_ui.selection()),\n        );\n    }\n}\n"
  },
  {
    "path": "src/gui/top_menu/file.rs",
    "content": "use {\n    crate::{\n        app::{App, set_clipboard_string},\n        gui::{Gui, dialogs::AutoSaveReloadDialog},\n        shell::msg_if_fail,\n    },\n    constcat::concat,\n    egui::Button,\n    egui_phosphor::regular as ic,\n};\n\nconst L_LOPEN: &str = concat!(ic::FOLDER_OPEN, \" Open...\");\nconst L_OPEN_PROCESS: &str = concat!(ic::CPU, \" Open process...\");\nconst L_OPEN_PREVIOUS: &str = concat!(ic::ARROWS_LEFT_RIGHT, \" Open previous\");\nconst L_SAVE: &str = concat!(ic::FLOPPY_DISK, \" Save\");\nconst L_SAVE_AS: &str = concat!(ic::FLOPPY_DISK_BACK, \" Save as...\");\nconst L_RELOAD: &str = concat!(ic::ARROW_COUNTER_CLOCKWISE, \" Reload\");\nconst L_RECENT: &str = concat!(ic::CLOCK_COUNTER_CLOCKWISE, \" Recent\");\nconst L_AUTO_SAVE_RELOAD: &str = concat!(ic::MAGNET, \" Auto save/reload...\");\nconst L_CREATE_BACKUP: &str = concat!(ic::CLOUD_ARROW_UP, \" Create backup\");\nconst L_RESTORE_BACKUP: &str = concat!(ic::CLOUD_ARROW_DOWN, \" Restore backup\");\nconst L_PREFERENCES: &str = concat!(ic::GEAR_SIX, \" Preferences\");\nconst L_CLOSE: &str = concat!(ic::X_SQUARE, \" Close\");\nconst L_QUIT: &str = concat!(ic::SIGN_OUT, \" Quit\");\n\npub fn ui(ui: &mut egui::Ui, gui: &mut Gui, app: &mut App, font_size: u16, line_spacing: u16) {\n    if ui.add(Button::new(L_LOPEN).shortcut_text(\"Ctrl+O\")).clicked() {\n        gui.fileops.load_file(app.source_file());\n    }\n    if ui.button(L_OPEN_PROCESS).clicked() {\n        gui.win.open_process.open.toggle();\n    }\n    let mut load = None;\n    if ui\n        .add_enabled(\n            !app.cfg.recent.is_empty(),\n            Button::new(L_OPEN_PREVIOUS).shortcut_text(\"Ctrl+P\"),\n        )\n        .on_hover_text(\"Can be used to switch between 2 files quickly for comparison\")\n        .clicked()\n    {\n        crate::shell::open_previous(app, &mut load);\n    }\n    ui.checkbox(&mut app.preferences.keep_meta, \"Keep metadata\")\n        .on_hover_text(\"Keep metadata when loading a new file\");\n    ui.menu_button(L_RECENT, |ui| {\n        app.cfg.recent.retain(|entry| {\n            let mut retain = true;\n            let path = entry.file.as_ref().map_or_else(\n                || String::from(\"Unnamed file\"),\n                |path| path.display().to_string(),\n            );\n            ui.horizontal(|ui| {\n                if ui.button(&path).clicked() {\n                    load = Some(entry.clone());\n                }\n                ui.separator();\n                if ui.button(\"📋\").clicked() {\n                    set_clipboard_string(&mut app.clipboard, &mut gui.msg_dialog, &path);\n                }\n                if ui.button(\"🗑\").clicked() {\n                    retain = false;\n                }\n            });\n            ui.separator();\n            retain\n        });\n        ui.separator();\n        ui.horizontal(|ui| {\n            let mut cap = app.cfg.recent.capacity();\n            if ui.add(egui::DragValue::new(&mut cap).prefix(\"list capacity: \")).changed() {\n                app.cfg.recent.set_capacity(cap);\n            }\n            ui.separator();\n            if ui.add_enabled(!app.cfg.recent.is_empty(), Button::new(\"🗑 Clear all\")).clicked() {\n                app.cfg.recent.clear();\n            }\n        });\n    });\n    if let Some(args) = load {\n        app.load_file_args(\n            args,\n            None,\n            &mut gui.msg_dialog,\n            font_size,\n            line_spacing,\n            None,\n        );\n    }\n    ui.separator();\n    if ui\n        .add_enabled(\n            matches!(&app.source, Some(src) if src.attr.permissions.write)\n                && app.data.dirty_region.is_some(),\n            Button::new(L_SAVE).shortcut_text(\"Ctrl+S\"),\n        )\n        .clicked()\n    {\n        msg_if_fail(\n            app.save(&mut gui.msg_dialog),\n            \"Failed to save\",\n            &mut gui.msg_dialog,\n        );\n    }\n    if ui.button(L_SAVE_AS).clicked() {\n        gui.fileops.save_file_as();\n    }\n    if ui.add(Button::new(L_RELOAD).shortcut_text(\"Ctrl+R\")).clicked() {\n        msg_if_fail(app.reload(), \"Failed to reload\", &mut gui.msg_dialog);\n    }\n    if ui.button(L_AUTO_SAVE_RELOAD).clicked() {\n        Gui::add_dialog(&mut gui.dialogs, AutoSaveReloadDialog);\n    }\n    ui.separator();\n    if ui.button(L_CREATE_BACKUP).clicked() {\n        msg_if_fail(\n            app.create_backup(),\n            \"Failed to create backup\",\n            &mut gui.msg_dialog,\n        );\n    }\n    if ui.button(L_RESTORE_BACKUP).clicked() {\n        msg_if_fail(\n            app.restore_backup(),\n            \"Failed to restore backup\",\n            &mut gui.msg_dialog,\n        );\n    }\n    ui.separator();\n    if ui.button(L_PREFERENCES).clicked() {\n        gui.win.preferences.open.toggle();\n    }\n    ui.separator();\n    if ui.add(Button::new(L_CLOSE).shortcut_text(\"Ctrl+W\")).clicked() {\n        app.close_file();\n    }\n    if ui.button(L_QUIT).clicked() {\n        app.quit_requested = true;\n    }\n}\n"
  },
  {
    "path": "src/gui/top_menu/help.rs",
    "content": "use {\n    crate::{gui::Gui, shell::msg_if_fail},\n    constcat::concat,\n    egui::{Button, Ui},\n    egui_phosphor::regular as ic,\n    gamedebug_core::{IMMEDIATE, PERSISTENT},\n};\n\nconst L_HEXERATOR_BOOK: &str = concat!(ic::BOOK_OPEN_TEXT, \" Hexerator book\");\nconst L_DEBUG_PANEL: &str = concat!(ic::BUG, \" Debug panel...\");\nconst L_ABOUT: &str = concat!(ic::QUESTION, \" About Hexerator...\");\n\npub fn ui(ui: &mut Ui, gui: &mut Gui) {\n    if ui.button(L_HEXERATOR_BOOK).clicked() {\n        msg_if_fail(\n            open::that(crate::gui::BOOK_URL),\n            \"Failed to open help\",\n            &mut gui.msg_dialog,\n        );\n    }\n    if ui.add(Button::new(L_DEBUG_PANEL).shortcut_text(\"F12\")).clicked() {\n        IMMEDIATE.toggle();\n        PERSISTENT.toggle();\n    }\n    ui.separator();\n    if ui.button(L_ABOUT).clicked() {\n        gui.win.about.open.toggle();\n    }\n}\n"
  },
  {
    "path": "src/gui/top_menu/meta.rs",
    "content": "use {\n    crate::{\n        app::App,\n        gui::{Gui, egui_ui_ext::EguiResponseExt as _},\n        shell::msg_if_fail,\n    },\n    constcat::concat,\n    egui::Button,\n    egui_phosphor::regular as ic,\n};\n\nconst L_PERSPECTIVES: &str = concat!(ic::PERSPECTIVE, \" Perspectives...\");\nconst L_REGIONS: &str = concat!(ic::RULER, \" Regions...\");\nconst L_BOOKMARKS: &str = concat!(ic::BOOKMARK, \" Bookmarks...\");\nconst L_VARIABLES: &str = concat!(ic::CALCULATOR, \" Variables...\");\nconst L_STRUCTS: &str = concat!(ic::BLUEPRINT, \" Structs...\");\nconst L_RELOAD: &str = concat!(ic::ARROW_COUNTER_CLOCKWISE, \" Reload\");\nconst L_LOAD_FROM_FILE: &str = concat!(ic::FOLDER_OPEN, \" Load from file...\");\nconst L_LOAD_FROM_BACKUP: &str = concat!(ic::CLOUD_ARROW_DOWN, \" Load from temp backup\");\nconst L_CLEAR: &str = concat!(ic::BROOM, \" Clear\");\nconst L_DIFF_WITH_CLEAN_META: &str = concat!(ic::GIT_DIFF, \" Diff with clean meta\");\nconst L_SAVE: &str = concat!(ic::FLOPPY_DISK, \" Save\");\nconst L_SAVE_AS: &str = concat!(ic::FLOPPY_DISK_BACK, \" Save as...\");\nconst L_ASSOCIATE_WITH_CURRENT: &str = concat!(ic::FLOW_ARROW, \" Associate with current file\");\n\npub fn ui(ui: &mut egui::Ui, gui: &mut Gui, app: &mut App, font_size: u16, line_spacing: u16) {\n    if ui.add(Button::new(L_PERSPECTIVES).shortcut_text(\"F7\")).clicked() {\n        gui.win.perspectives.open.toggle();\n    }\n    if ui.add(Button::new(L_REGIONS).shortcut_text(\"F8\")).clicked() {\n        gui.win.regions.open.toggle();\n    }\n    if ui.add(Button::new(L_BOOKMARKS).shortcut_text(\"F9\")).clicked() {\n        gui.win.bookmarks.open.toggle();\n    }\n    if ui.add(Button::new(L_VARIABLES).shortcut_text(\"F10\")).clicked() {\n        gui.win.vars.open.toggle();\n    }\n    if ui.add(Button::new(L_STRUCTS).shortcut_text(\"F11\")).clicked() {\n        gui.win.structs.open.toggle();\n    }\n    ui.separator();\n    if ui\n        .button(L_DIFF_WITH_CLEAN_META)\n        .on_hover_text(\"See and manage changes to metafile\")\n        .clicked()\n    {\n        gui.win.meta_diff.open.toggle();\n    }\n    ui.separator();\n    if ui\n        .add_enabled(\n            !app.meta_state.current_meta_path.as_os_str().is_empty(),\n            Button::new(L_RELOAD),\n        )\n        .on_hover_text_deferred(|| {\n            format!(\"Reload from {}\", app.meta_state.current_meta_path.display())\n        })\n        .clicked()\n    {\n        msg_if_fail(\n            app.consume_meta_from_file(app.meta_state.current_meta_path.clone(), false),\n            \"Failed to load metafile\",\n            &mut gui.msg_dialog,\n        );\n    }\n    if ui.button(L_LOAD_FROM_FILE).clicked() {\n        gui.fileops.load_meta_file();\n    }\n    if ui\n        .button(L_LOAD_FROM_BACKUP)\n        .on_hover_text(\"Load from temporary backup (auto generated on save/exit)\")\n        .clicked()\n    {\n        msg_if_fail(\n            app.consume_meta_from_file(crate::app::temp_metafile_backup_path(), true),\n            \"Failed to load temp metafile\",\n            &mut gui.msg_dialog,\n        );\n    }\n    if ui\n        .button(L_CLEAR)\n        .on_hover_text(\"Replace current meta with default one\")\n        .clicked()\n    {\n        app.clear_meta(font_size, line_spacing);\n    }\n    ui.separator();\n    if ui\n        .add_enabled(\n            !app.meta_state.current_meta_path.as_os_str().is_empty(),\n            Button::new(L_SAVE).shortcut_text(\"Ctrl+M\"),\n        )\n        .on_hover_text_deferred(|| {\n            format!(\"Save to {}\", app.meta_state.current_meta_path.display())\n        })\n        .clicked()\n    {\n        msg_if_fail(\n            app.save_meta(),\n            \"Failed to save metafile\",\n            &mut gui.msg_dialog,\n        );\n    }\n    if ui.button(L_SAVE_AS).clicked() {\n        gui.fileops.save_metafile_as();\n    }\n    ui.separator();\n    match (\n        app.source_file(),\n        app.meta_state.current_meta_path.as_os_str().is_empty(),\n    ) {\n        (Some(src), false) => {\n            if ui\n                .button(L_ASSOCIATE_WITH_CURRENT)\n                .on_hover_text(\"When you open this file, it will use this metafile\")\n                .clicked()\n            {\n                app.cfg\n                    .meta_assocs\n                    .insert(src.to_owned(), app.meta_state.current_meta_path.clone());\n            }\n        }\n        _ => {\n            ui.add_enabled(false, Button::new(L_ASSOCIATE_WITH_CURRENT))\n                .on_disabled_hover_text(\"Both file and metafile need to have a path\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/top_menu/perspective.rs",
    "content": "\n"
  },
  {
    "path": "src/gui/top_menu/plugins.rs",
    "content": "use crate::{\n    app::App,\n    gui::{Gui, message_dialog::Icon},\n    plugin::PluginContainer,\n    shell::msg_fail,\n};\n\npub fn ui(ui: &mut egui::Ui, gui: &mut Gui, app: &mut App) {\n    let mut plugins = std::mem::take(&mut app.plugins);\n    let mut reload = None;\n    if plugins.is_empty() {\n        ui.add_enabled(false, egui::Label::new(\"No plugins loaded\"));\n    }\n    plugins.retain_mut(|plugin| {\n        let mut retain = true;\n        ui.horizontal(|ui| {\n            ui.label(plugin.plugin.name()).on_hover_text(plugin.plugin.desc());\n            if ui.button(\"🗑\").on_hover_text(\"Unload\").clicked() {\n                retain = false;\n            }\n            if ui.button(\"↺\").on_hover_text(\"Reload\").clicked() {\n                retain = false;\n                reload = Some(plugin.path.clone());\n            }\n        });\n        for method in &plugin.methods {\n            let name = if let Some(name) = method.human_name {\n                name\n            } else {\n                method.method_name\n            };\n            let hover_ui = |ui: &mut egui::Ui| {\n                ui.horizontal(|ui| {\n                    ui.style_mut().spacing.item_spacing.x = 0.;\n                    ui.label(\n                        egui::RichText::new(method.method_name)\n                            .strong()\n                            .color(egui::Color32::WHITE),\n                    );\n                    ui.label(egui::RichText::new(\"(\").strong().color(egui::Color32::WHITE));\n                    for param in method.params {\n                        ui.label(format!(\"{}: {},\", param.name, param.ty.label()));\n                    }\n                    ui.label(egui::RichText::new(\")\").strong().color(egui::Color32::WHITE));\n                });\n                ui.indent(\"indent\", |ui| {\n                    ui.label(method.desc);\n                });\n            };\n            if ui.button(name).on_hover_ui(hover_ui).clicked() {\n                match plugin.plugin.on_method_called(method.method_name, &[], app) {\n                    Ok(val) => {\n                        if let Some(val) = val {\n                            let strval = match val {\n                                hexerator_plugin_api::Value::U64(n) => n.to_string(),\n                                hexerator_plugin_api::Value::String(s) => s.to_string(),\n                                hexerator_plugin_api::Value::F64(n) => n.to_string(),\n                            };\n                            gui.msg_dialog.open(\n                                Icon::Info,\n                                \"Method call result\",\n                                format!(\"{}: {}\", method.method_name, strval),\n                            );\n                        }\n                    }\n                    Err(e) => {\n                        msg_fail(&e, \"Method call failed\", &mut gui.msg_dialog);\n                    }\n                }\n            }\n        }\n        retain\n    });\n    if let Some(path) = reload {\n        // Safety: This will cause UB on a bad plugin. Nothing we can do.\n        //\n        // It's up to the user not to load bad plugins.\n        unsafe {\n            match PluginContainer::new(path) {\n                Ok(plugin) => {\n                    plugins.push(plugin);\n                }\n                Err(e) => msg_fail(&e, \"Failed to reload plugin\", &mut gui.msg_dialog),\n            }\n        }\n    }\n    std::mem::swap(&mut app.plugins, &mut plugins);\n}\n"
  },
  {
    "path": "src/gui/top_menu/scripting.rs",
    "content": "use {\n    crate::{app::App, gui::Gui, shell::msg_if_fail},\n    mlua::Lua,\n};\n\npub fn ui(\n    ui: &mut egui::Ui,\n    gui: &mut Gui,\n    app: &mut App,\n    lua: &Lua,\n    font_size: u16,\n    line_spacing: u16,\n) {\n    if ui.button(\"🖹 Lua editor\").clicked() {\n        gui.win.lua_editor.open.toggle();\n    }\n    if ui.button(\"📃 Script manager\").clicked() {\n        gui.win.script_manager.open.toggle();\n    }\n    if ui.button(\"🖳 Quick eval window\").clicked() {\n        gui.win.lua_console.open.toggle();\n    }\n    if ui.button(\"👁 New watch window\").clicked() {\n        gui.win.add_lua_watch_window();\n    }\n    if ui.button(\"？ Hexerator Lua API\").clicked() {\n        gui.win.lua_help.open.toggle();\n    }\n    ui.separator();\n    let mut scripts = std::mem::take(&mut app.meta_state.meta.scripts);\n    for (key, script) in scripts.iter() {\n        if ui.button(&script.name).clicked() {\n            let result = crate::scripting::exec_lua(\n                lua,\n                &script.content,\n                app,\n                gui,\n                \"\",\n                Some(key),\n                font_size,\n                line_spacing,\n            );\n            msg_if_fail(result, \"Failed to execute script\", &mut gui.msg_dialog);\n        }\n    }\n    std::mem::swap(&mut app.meta_state.meta.scripts, &mut scripts);\n}\n"
  },
  {
    "path": "src/gui/top_menu/view.rs",
    "content": "use {\n    crate::{app::App, gui::Gui, hex_ui::Ruler, meta::LayoutMapExt as _},\n    constcat::concat,\n    egui::{\n        Button,\n        color_picker::{Alpha, color_picker_color32},\n        containers::menu::{MenuConfig, SubMenuButton},\n    },\n    egui_phosphor::regular as ic,\n};\n\nconst L_LAYOUT: &str = concat!(ic::LAYOUT, \" Layout\");\nconst L_RULER: &str = concat!(ic::RULER, \" Ruler\");\nconst L_LAYOUTS: &str = concat!(ic::LAYOUT, \" Layouts...\");\nconst L_FOCUS_PREV: &str = concat!(ic::ARROW_FAT_LEFT, \" Focus previous\");\nconst L_FOCUS_NEXT: &str = concat!(ic::ARROW_FAT_RIGHT, \" Focus next\");\nconst L_VIEWS: &str = concat!(ic::EYE, \" Views...\");\n\npub fn ui(ui: &mut egui::Ui, gui: &mut Gui, app: &mut App) {\n    if ui.add(Button::new(L_VIEWS).shortcut_text(\"F6\")).clicked() {\n        gui.win.views.open.toggle();\n    }\n    if ui.add(Button::new(L_FOCUS_PREV).shortcut_text(\"Shift+Tab\")).clicked() {\n        app.focus_prev_view_in_layout();\n    }\n    if ui.add(Button::new(L_FOCUS_NEXT).shortcut_text(\"Tab\")).clicked() {\n        app.focus_next_view_in_layout();\n    }\n    ui.menu_button(L_RULER, |ui| match app.focused_view_mut() {\n        Some((key, _view)) => match app.hex_ui.rulers.get_mut(&key) {\n            Some(ruler) => {\n                if ui.button(\"Remove\").clicked() {\n                    app.hex_ui.rulers.remove(&key);\n                    return;\n                }\n                ruler.color.with_as_egui_mut(|c| {\n                    // Customized color SubMenuButton (taken from the egui demo)\n                    let is_bright = c.intensity() > 0.5;\n                    let text_color = if is_bright {\n                        egui::Color32::BLACK\n                    } else {\n                        egui::Color32::WHITE\n                    };\n                    let mut color_button =\n                        SubMenuButton::new(egui::RichText::new(\"Color\").color(text_color)).config(\n                            MenuConfig::new()\n                                .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside),\n                        );\n                    color_button.button = color_button.button.fill(*c);\n                    color_button.ui(ui, |ui| {\n                        ui.spacing_mut().slider_width = 200.0;\n                        color_picker_color32(ui, c, Alpha::Opaque);\n                    });\n                });\n                ui.label(\"Frequency\");\n                ui.add(egui::DragValue::new(&mut ruler.freq));\n                ui.label(\"Horizontal offset\");\n                ui.add(egui::DragValue::new(&mut ruler.hoffset));\n                ui.menu_button(\"struct\", |ui| {\n                    for (i, struct_) in app.meta_state.meta.structs.iter().enumerate() {\n                        if ui.selectable_label(ruler.struct_idx == Some(i), &struct_.name).clicked()\n                        {\n                            ruler.struct_idx = Some(i);\n                        }\n                    }\n                    ui.separator();\n                    if ui.button(\"Unassociate\").clicked() {\n                        ruler.struct_idx = None;\n                    }\n                });\n            }\n            None => {\n                if ui.button(\"Add ruler for current view\").clicked() {\n                    app.hex_ui.rulers.insert(key, Ruler::default());\n                }\n            }\n        },\n        None => {\n            ui.label(\"<No active view>\");\n        }\n    });\n    ui.separator();\n    ui.menu_button(L_LAYOUT, |ui| {\n        if ui.add(Button::new(L_LAYOUTS).shortcut_text(\"F5\")).clicked() {\n            gui.win.layouts.open.toggle();\n        }\n        if ui.button(\"➕ Add new\").clicked() {\n            app.hex_ui.current_layout = app.meta_state.meta.layouts.add_new_default();\n            gui.win.layouts.open.set(true);\n        }\n        ui.separator();\n        for (k, v) in &app.meta_state.meta.layouts {\n            if ui\n                .selectable_label(\n                    app.hex_ui.current_layout == k,\n                    [ic::LAYOUT, \" \", v.name.as_str()].concat(),\n                )\n                .clicked()\n            {\n                App::switch_layout(&mut app.hex_ui, &app.meta_state.meta, k);\n            }\n        }\n    });\n    ui.checkbox(\n        &mut app.preferences.col_change_lock_col,\n        \"Lock col on col change\",\n    );\n    ui.checkbox(\n        &mut app.preferences.col_change_lock_row,\n        \"Lock row on col change\",\n    );\n}\n"
  },
  {
    "path": "src/gui/top_menu.rs",
    "content": "use {crate::shell::msg_if_fail, mlua::Lua};\n\nmod analysis;\nmod cursor;\npub mod edit;\nmod file;\nmod help;\nmod meta;\nmod perspective;\nmod plugins;\nmod scripting;\nmod view;\n\nuse {\n    crate::{app::App, source::SourceProvider},\n    egui::Layout,\n};\n\npub fn top_menu(\n    ui: &mut egui::Ui,\n    gui: &mut crate::gui::Gui,\n    app: &mut App,\n    lua: &Lua,\n    font_size: u16,\n    line_spacing: u16,\n) {\n    ui.horizontal(|ui| {\n        ui.menu_button(\"File\", |ui| file::ui(ui, gui, app, font_size, line_spacing));\n        ui.menu_button(\"Edit\", |ui| {\n            edit::ui(ui, gui, app, lua, font_size, line_spacing);\n        });\n        ui.menu_button(\"Cursor\", |ui| cursor::ui(ui, gui, app));\n        ui.menu_button(\"View\", |ui| view::ui(ui, gui, app));\n        ui.menu_button(\"Meta\", |ui| meta::ui(ui, gui, app, font_size, line_spacing));\n        ui.menu_button(\"Analysis\", |ui| analysis::ui(ui, gui, app));\n        ui.menu_button(\"Lua scripting\", |ui| {\n            scripting::ui(ui, gui, app, lua, font_size, line_spacing);\n        });\n        ui.menu_button(\"Plugins\", |ui| plugins::ui(ui, gui, app));\n        ui.menu_button(\"Help\", |ui| help::ui(ui, gui));\n        ui.with_layout(\n            Layout::right_to_left(egui::Align::Center),\n            |ui| match &app.source {\n                Some(src) => {\n                    match src.provider {\n                        SourceProvider::File(_) => {\n                            match &app.src_args.file {\n                                Some(file) => {\n                                    let s = file.display().to_string();\n                                    let ctx_menu = |ui: &mut egui::Ui| {\n                                        if ui.button(\"Open\").clicked() {\n                                            try_open_file(file, gui);\n                                        }\n                                        if ui.button(\"Copy path to clipboard\").clicked() {\n                                            crate::app::set_clipboard_string(\n                                                &mut app.clipboard,\n                                                &mut gui.msg_dialog,\n                                                &s,\n                                            );\n                                        }\n                                        if let Some(parent) = file.parent() {\n                                            if ui.button(\"Open containing folder\").clicked() {\n                                                let result = open::that(parent);\n                                                msg_if_fail(\n                                                    result,\n                                                    \"Failed to open folder\",\n                                                    &mut gui.msg_dialog,\n                                                );\n                                            }\n                                            if ui.button(\"Copy folder path to clipboard\").clicked()\n                                            {\n                                                crate::app::set_clipboard_string(\n                                                    &mut app.clipboard,\n                                                    &mut gui.msg_dialog,\n                                                    &parent.display().to_string(),\n                                                );\n                                            }\n                                        }\n                                    };\n                                    if !app.meta_state.current_meta_path.as_os_str().is_empty() {\n                                        ui.label(\n                                            egui::RichText::new(\"🇲\").color(egui::Color32::YELLOW),\n                                        )\n                                        .on_hover_text(\n                                            format!(\n                                                \"Metafile: {}\",\n                                                app.meta_state.current_meta_path.display()\n                                            ),\n                                        );\n                                    } else {\n                                        ui.label(\"？\").on_hover_text(\n                                            \"There is no metafile associated with this file\",\n                                        );\n                                    }\n                                    let mut re =\n                                        ui.add(egui::Label::new(&s).sense(egui::Sense::click()));\n                                    re.context_menu(ctx_menu);\n                                    re = re.on_hover_ui(|ui| {\n                                        ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n                                        if let Some(offset) = &app.src_args.hard_seek {\n                                            ui.label(format!(\"Hard seek: {offset} ({offset:X})\"));\n                                        }\n                                        if let Some(len) = &app.src_args.take {\n                                            ui.label(format!(\"Take: {len}\"));\n                                        }\n                                        ui.collapsing(\"Src args\", |ui| {\n                                            ui.code(format!(\"{:#?}\", app.src_args));\n                                        });\n                                        ui.collapsing(\"Source\", |ui| {\n                                            ui.code(format!(\"{src:#?}\"));\n                                        });\n                                        ui.collapsing(\"Data provider\", |ui| {\n                                            ui.code(format!(\"{:#?}\", app.data));\n                                        });\n                                        ui.label(\"Right click for context menu\");\n                                    });\n                                    if re.clicked() {\n                                        try_open_file(file, gui);\n                                    }\n                                }\n                                None => {\n                                    ui.label(\"File path unknown\");\n                                }\n                            };\n                        }\n                        SourceProvider::Stdin(_) => {\n                            ui.label(\"Standard input\");\n                        }\n                        #[cfg(windows)]\n                        SourceProvider::WinProc { handle, .. } => {\n                            ui.label(format!(\"Windows process: {:p}\", handle));\n                        }\n                    }\n                    if src.attr.stream {\n                        if src.state.stream_end {\n                            ui.label(\"[finished stream]\");\n                        } else {\n                            ui.spinner();\n                            ui.label(\"[streaming]\");\n                        }\n                    }\n                }\n                None => {\n                    ui.label(\"No source\");\n                }\n            },\n        );\n    });\n}\n\nfn try_open_file(file: &std::path::Path, gui: &mut super::Gui) {\n    let result = open::that(file);\n    msg_if_fail(result, \"Failed to open file\", &mut gui.msg_dialog);\n}\n"
  },
  {
    "path": "src/gui/top_panel.rs",
    "content": "use {\n    super::{\n        Gui, dialogs::LuaColorDialog, egui_ui_ext::EguiResponseExt as _, message_dialog::Icon,\n        top_menu::top_menu,\n    },\n    crate::{\n        app::App,\n        color::RgbColor,\n        util::human_size,\n        value_color::{ColorMethod, Palette},\n    },\n    anyhow::Context as _,\n    egui::{ComboBox, Layout, Ui},\n    mlua::Lua,\n};\n\npub fn ui(ui: &mut Ui, gui: &mut Gui, app: &mut App, lua: &Lua, font_size: u16, line_spacing: u16) {\n    top_menu(ui, gui, app, lua, font_size, line_spacing);\n    ui.horizontal(|ui| {\n        if app.hex_ui.select_a.is_some() || app.hex_ui.select_b.is_some() {\n            ui.label(\"Selection\");\n        }\n        let mut action_focus = None;\n        if let Some(a) = &mut app.hex_ui.select_a {\n            if ui.link(\"a\").clicked() {\n                action_focus = Some(*a);\n            }\n            ui.add(egui::DragValue::new(a));\n        }\n        if let Some(b) = &mut app.hex_ui.select_b {\n            if ui.link(\"b\").clicked() {\n                action_focus = Some(*b);\n            }\n            ui.add(egui::DragValue::new(b));\n        }\n        if let Some(off) = action_focus {\n            app.search_focus(off);\n        }\n        if let Some(sel) = app.hex_ui.selection()\n            && let Some(view_key) = app.hex_ui.focused_view\n        {\n            let view = &app.meta_state.meta.views[view_key].view;\n            let [rows, rem] =\n                app.meta_state.meta.low.perspectives[view.perspective].region_row_span(sel);\n            ui.label(format!(\n                \"{rows} rows * {} cols + {rem} = {}\",\n                app.meta_state.meta.low.perspectives[view.perspective].cols,\n                sel.len()\n            ))\n            .on_hover_text_deferred(|| human_size(sel.len()));\n            #[expect(clippy::collapsible_if)]\n            if ui.button(\"⬅ prev chunk\").clicked() {\n                if let Some(chk) = sel.prev_chunk() {\n                    app.hex_ui.select_a = Some(chk.begin);\n                    app.hex_ui.select_b = Some(chk.end);\n                }\n            }\n            if ui.button(\"next chunk ➡\").clicked() {\n                let chk = sel.next_chunk();\n                app.hex_ui.select_a = Some(chk.begin);\n                app.hex_ui.select_b = Some(chk.end);\n            }\n            if ui.button(\"Clear\").clicked() {\n                app.hex_ui.clear_selections();\n            }\n        }\n        if !app.hex_ui.extra_selections.is_empty() {\n            ui.label(format!(\n                \"({} extra selections)\",\n                app.hex_ui.extra_selections.len()\n            ));\n        }\n        if !gui.highlight_set.is_empty() {\n            ui.label(format!(\"{} bytes highlighted\", gui.highlight_set.len()));\n            if ui.button(\"Clear\").clicked() {\n                gui.highlight_set.clear();\n            }\n        }\n        if let Some(view_key) = app.hex_ui.focused_view {\n            let presentation = &mut app.meta_state.meta.views[view_key].view.presentation;\n            ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {\n                ui.checkbox(&mut presentation.invert_color, \"invert\");\n                ComboBox::new(\"color_combo\", \"Color\")\n                    .selected_text(presentation.color_method.name())\n                    .show_ui(ui, |ui| {\n                        ui.selectable_value(\n                            &mut presentation.color_method,\n                            ColorMethod::Default,\n                            ColorMethod::Default.name(),\n                        );\n                        ui.selectable_value(\n                            &mut presentation.color_method,\n                            ColorMethod::Pure,\n                            ColorMethod::Pure.name(),\n                        );\n                        ui.selectable_value(\n                            &mut presentation.color_method,\n                            ColorMethod::Mono(RgbColor::WHITE),\n                            ColorMethod::Mono(RgbColor::WHITE).name(),\n                        );\n                        ui.selectable_value(\n                            &mut presentation.color_method,\n                            ColorMethod::Rgb332,\n                            ColorMethod::Rgb332.name(),\n                        );\n                        ui.selectable_value(\n                            &mut presentation.color_method,\n                            ColorMethod::Vga13h,\n                            ColorMethod::Vga13h.name(),\n                        );\n                        ui.selectable_value(\n                            &mut presentation.color_method,\n                            ColorMethod::BrightScale(RgbColor::WHITE),\n                            ColorMethod::BrightScale(RgbColor::WHITE).name(),\n                        );\n                        if ui\n                            .selectable_label(\n                                matches!(&presentation.color_method, ColorMethod::Custom(..)),\n                                \"custom\",\n                            )\n                            .clicked()\n                        {\n                            #[expect(\n                                clippy::cast_possible_truncation,\n                                reason = \"The array is 256 elements long\"\n                            )]\n                            let arr = std::array::from_fn(|i| {\n                                let c = presentation\n                                    .color_method\n                                    .byte_color(i as u8, presentation.invert_color);\n                                [c.r, c.g, c.b]\n                            });\n                            presentation.color_method = ColorMethod::Custom(Box::new(Palette(arr)));\n                        }\n                    });\n                ui.color_edit_button_rgb(&mut app.preferences.bg_color);\n                ui.label(\"Bg color\");\n                if let ColorMethod::Mono(color) | ColorMethod::BrightScale(color) =\n                    &mut presentation.color_method\n                {\n                    let mut rgb = [color.r, color.g, color.b];\n                    ui.color_edit_button_srgb(&mut rgb);\n                    [color.r, color.g, color.b] = rgb;\n                    ui.label(\"Text color\");\n                }\n                if let ColorMethod::Custom(arr) = &mut presentation.color_method {\n                    let Some(&byte) = app.data.get(app.edit_state.cursor) else {\n                        return;\n                    };\n                    let col = &mut arr.0[byte as usize];\n                    ui.color_edit_button_srgb(col);\n                    ui.label(\"Byte color\");\n                    if ui.button(\"#\").on_hover_text(\"From hex code on clipboard\").clicked() {\n                        match color_from_hexcode(&crate::app::get_clipboard_string(\n                            &mut app.clipboard,\n                            &mut gui.msg_dialog,\n                        )) {\n                            Ok(new) => *col = new,\n                            Err(e) => {\n                                gui.msg_dialog.open(\n                                    Icon::Error,\n                                    \"Color parse error\",\n                                    e.to_string(),\n                                );\n                            }\n                        }\n                    }\n                    if ui.button(\"Lua\").on_hover_text(\"From lua script\").clicked() {\n                        Gui::add_dialog(&mut gui.dialogs, LuaColorDialog::default());\n                    }\n                    if ui.button(\"Save\").clicked() {\n                        gui.fileops.save_palette_for_view(view_key);\n                    }\n                    if ui.button(\"Load\").clicked() {\n                        gui.fileops.load_palette_for_view(view_key);\n                    }\n                    let tooltip = \"\\\n                    From image file.\\n\\\n                    \\n\\\n                    Pixel by pixel, the image's colors will become the byte colors.\n                    \";\n                    if ui\n                        .add_enabled(app.hex_ui.selection().is_some(), egui::Button::new(\"img\"))\n                        .on_hover_text(tooltip)\n                        .clicked()\n                    {\n                        gui.fileops.load_palette_from_image_for_view(view_key);\n                    }\n                }\n            });\n        }\n    });\n}\n\nfn color_from_hexcode(mut src: &str) -> anyhow::Result<[u8; 3]> {\n    let mut out = [0; 3];\n    src = src.trim_start_matches('#');\n    for (i, byte) in out.iter_mut().enumerate() {\n        let src_idx = i * 2;\n        *byte = u8::from_str_radix(src.get(src_idx..src_idx + 2).context(\"Indexing error\")?, 16)?;\n    }\n    Ok(out)\n}\n\n#[test]\n#[expect(clippy::unwrap_used)]\nfn test_color_from_hexcode() {\n    assert_eq!(color_from_hexcode(\"#ffffff\").unwrap(), [255, 255, 255]);\n    assert_eq!(color_from_hexcode(\"ff00ff\").unwrap(), [255, 0, 255]);\n}\n"
  },
  {
    "path": "src/gui/windows/about.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{result_ext::AnyhowConv as _, shell::msg_if_fail},\n    egui_extras::{Column, TableBuilder},\n    std::fmt::Write as _,\n    sysinfo::System,\n};\n\ntype InfoPair = (&'static str, String);\n\npub struct AboutWindow {\n    pub open: WindowOpen,\n    sys: System,\n    info: [InfoPair; 14],\n    os_name: String,\n    os_ver: String,\n}\n\nimpl Default for AboutWindow {\n    fn default() -> Self {\n        Self {\n            open: Default::default(),\n            sys: Default::default(),\n            info: Default::default(),\n            os_name: System::name().unwrap_or_else(|| \"Unknown\".into()),\n            os_ver: System::os_version().unwrap_or_else(|| \"Unknown version\".into()),\n        }\n    }\n}\n\nconst MIB: u64 = 1_048_576;\n\nmacro_rules! optenv {\n    ($name:literal) => {\n        option_env!($name).unwrap_or(\"<unavailable>\").to_string()\n    };\n}\n\nimpl super::Window for AboutWindow {\n    fn ui(&mut self, WinCtx { ui, gui, app, .. }: WinCtx) {\n        ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n        if self.open.just_now() {\n            self.sys.refresh_cpu_all();\n            self.sys.refresh_memory();\n            self.info = [\n                (\"Hexerator\", String::new()),\n                (\"Version\", optenv!(\"CARGO_PKG_VERSION\")),\n                (\"Git SHA\", optenv!(\"VERGEN_GIT_SHA\")),\n                (\n                    \"Commit date\",\n                    optenv!(\"VERGEN_GIT_COMMIT_TIMESTAMP\")\n                        .split('T')\n                        .next()\n                        .unwrap_or(\"error\")\n                        .into(),\n                ),\n                (\n                    \"Build date\",\n                    optenv!(\"VERGEN_BUILD_TIMESTAMP\").split('T').next().unwrap_or(\"error\").into(),\n                ),\n                (\"Target\", optenv!(\"VERGEN_CARGO_TARGET_TRIPLE\")),\n                (\"Debug\", optenv!(\"VERGEN_CARGO_DEBUG\")),\n                (\"Opt-level\", optenv!(\"VERGEN_CARGO_OPT_LEVEL\")),\n                (\"Built with rustc\", optenv!(\"VERGEN_RUSTC_SEMVER\")),\n                (\"System\", String::new()),\n                (\"OS\", format!(\"{} {}\", self.os_name, self.os_ver)),\n                (\n                    \"Total memory\",\n                    format!(\"{} MiB\", self.sys.total_memory() / MIB),\n                ),\n                (\n                    \"Used memory\",\n                    format!(\"{} MiB\", self.sys.used_memory() / MIB),\n                ),\n                (\n                    \"Available memory\",\n                    format!(\"{} MiB\", self.sys.available_memory() / MIB),\n                ),\n            ];\n        }\n        info_table(ui, &self.info);\n        ui.separator();\n        ui.vertical_centered_justified(|ui| {\n            if ui.button(\"Copy to clipboard\").clicked() {\n                crate::app::set_clipboard_string(\n                    &mut app.clipboard,\n                    &mut gui.msg_dialog,\n                    &clipfmt_info(&self.info),\n                );\n            }\n        });\n        ui.separator();\n        ui.heading(\"Links\");\n        ui.vertical_centered_justified(|ui| {\n            let result = try {\n                if ui.link(\"📖 Book\").clicked() {\n                    open::that(crate::gui::BOOK_URL).how()?;\n                }\n                if ui.link(\" Git repository\").clicked() {\n                    open::that(\"https://github.com/crumblingstatue/hexerator/\").how()?;\n                }\n                if ui.link(\"💬 Discussions forum\").clicked() {\n                    open::that(\"https://github.com/crumblingstatue/hexerator/discussions\").how()?;\n                }\n            };\n            msg_if_fail(result, \"Failed to open link\", &mut gui.msg_dialog);\n            ui.separator();\n            if ui.button(\"Close\").clicked() {\n                self.open.set(false);\n            }\n        });\n    }\n\n    fn title(&self) -> &str {\n        \"About Hexerator\"\n    }\n}\n\nfn info_table(ui: &mut egui::Ui, info: &[InfoPair]) {\n    ui.push_id(info.as_ptr(), |ui| {\n        let body_height = ui.text_style_height(&egui::TextStyle::Body);\n        TableBuilder::new(ui)\n            .column(Column::auto())\n            .column(Column::remainder())\n            .resizable(true)\n            .striped(true)\n            .body(|mut body| {\n                for (k, v) in info {\n                    body.row(body_height + 2.0, |mut row| {\n                        row.col(|ui| {\n                            ui.label(*k);\n                        });\n                        row.col(|ui| {\n                            ui.label(v);\n                        });\n                    });\n                }\n            });\n    });\n}\n\nfn clipfmt_info(info: &[InfoPair]) -> String {\n    let mut out = String::new();\n    for (k, v) in info {\n        let _ = writeln!(out, \"{k}: {v}\");\n    }\n    out\n}\n"
  },
  {
    "path": "src/gui/windows/bookmarks.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        app::set_clipboard_string,\n        damage_region::DamageRegion,\n        data::Data,\n        gui::{message_dialog::MessageDialog, windows::regions::region_context_menu},\n        meta::{\n            Bookmark, find_most_specific_region_for_offset,\n            value_type::{\n                EndianedPrimitive, F32Be, F32Le, F64Be, F64Le, I8, I16Be, I16Le, I32Be, I32Le,\n                I64Be, I64Le, StringMap, U8, U16Be, U16Le, U32Be, U32Le, U64Be, U64Le, ValueType,\n            },\n        },\n        result_ext::AnyhowConv as _,\n        shell::{msg_fail, msg_if_fail},\n    },\n    anyhow::Context as _,\n    egui::{ScrollArea, Ui, text::CCursorRange},\n    egui_extras::{Column, TableBuilder},\n    gamedebug_core::per,\n    num_traits::AsPrimitive,\n    std::mem::discriminant,\n};\n\n#[derive(Default)]\npub struct BookmarksWindow {\n    pub open: WindowOpen,\n    pub selected: Option<usize>,\n    pub edit_name: bool,\n    pub focus_text_edit: bool,\n    value_type_string_buf: String,\n    name_filter_string: String,\n    autoreload: bool,\n}\n\nimpl super::Window for BookmarksWindow {\n    fn ui(&mut self, WinCtx { ui, gui, app, .. }: WinCtx) {\n        ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n        ui.horizontal(|ui| {\n            ui.add(\n                egui::TextEdit::singleline(&mut self.name_filter_string)\n                    .hint_text(\"Filter by name\"),\n            );\n            if ui.button(\"Highlight all\").clicked() {\n                gui.highlight_set.clear();\n                for bm in &app.meta_state.meta.bookmarks {\n                    gui.highlight_set.insert(bm.offset);\n                }\n            }\n            ui.checkbox(&mut self.autoreload, \"Autoreload\")\n                .on_hover_text(\"Automatically reload data every frame for the visible bookmarks\");\n        });\n        let mut action = Action::None;\n        ScrollArea::vertical().max_height(500.0).show(ui, |ui| {\n            TableBuilder::new(ui)\n                .columns(Column::auto(), 4)\n                .column(Column::remainder())\n                .striped(true)\n                .resizable(true)\n                .header(24.0, |mut row| {\n                    row.col(|ui| {\n                        ui.label(\"Name\");\n                    });\n                    row.col(|ui| {\n                        ui.label(\"Offset\");\n                    });\n                    row.col(|ui| {\n                        ui.label(\"Type\");\n                    });\n                    row.col(|ui| {\n                        ui.label(\"Value\");\n                    });\n                    row.col(|ui| {\n                        ui.label(\"Region\");\n                    });\n                })\n                .body(|body| {\n                    // Sort by offset\n                    let mut keys: Vec<usize> = (0..app.meta_state.meta.bookmarks.len()).collect();\n                    keys.sort_by_key(|&idx| app.meta_state.meta.bookmarks[idx].offset);\n                    keys.retain(|&k| {\n                        self.name_filter_string.is_empty()\n                            || app.meta_state.meta.bookmarks[k]\n                                .label\n                                .to_ascii_lowercase()\n                                .contains(&self.name_filter_string.to_ascii_lowercase())\n                    });\n                    body.rows(20.0, keys.len(), |mut row| {\n                        let idx = keys[row.index()];\n                        row.col(|ui| {\n                            let re = ui.selectable_label(\n                                self.selected == Some(idx),\n                                &app.meta_state.meta.bookmarks[idx].label,\n                            );\n                            re.context_menu(|ui| {\n                                if ui.button(\"Copy name to clipboard\").clicked() {\n                                    set_clipboard_string(\n                                        &mut app.clipboard,\n                                        &mut gui.msg_dialog,\n                                        &app.meta_state.meta.bookmarks[idx].label,\n                                    );\n                                }\n                            });\n                            if re.clicked() {\n                                self.selected = Some(idx);\n                            }\n                        });\n                        row.col(|ui| {\n                            let offset = app.meta_state.meta.bookmarks[idx].offset;\n                            let ctx_menu = |ui: &mut Ui| {\n                                if ui.button(\"Copy to clipboard\").clicked() {\n                                    set_clipboard_string(\n                                        &mut app.clipboard,\n                                        &mut gui.msg_dialog,\n                                        &offset.to_string(),\n                                    );\n                                }\n                                if ui\n                                    .button(\"Reoffset all bookmarks\")\n                                    .on_hover_text(\"Assume that the cursor is at the correct offset for this bookmark.\\n\\\n                                                    Reoffset all the other bookmarks based on that assumption.\").clicked() {\n                                        app.reoffset_bookmarks_cursor_diff(offset);\n                                }\n                            };\n                            let re = ui.link(offset.to_string());\n                            re.context_menu(ctx_menu);\n                            if re.clicked() {\n                                action = Action::Goto(offset);\n                            }\n                        });\n                        row.col(|ui| {\n                            ui.label(app.meta_state.meta.bookmarks[idx].value_type.label());\n                        });\n                        row.col(|ui| {\n                            let bookmark = &app.meta_state.meta.bookmarks[idx];\n                            let offs = bookmark.offset;\n                            if self.autoreload\n                                && let Err(e) = app.reload_range(offs, offs) {\n                                    eprintln!(\"Bookmark autoreload fail: {e}\");\n                                }\n                            let bookmark = &app.meta_state.meta.bookmarks[idx];\n                            let action = value_ui(\n                                bookmark,\n                                &mut app.data,\n                                ui,\n                                &mut app.clipboard,\n                                &mut gui.msg_dialog,\n                            );\n                            match action {\n                                Action::None => {}\n                                Action::Goto(offset) => app.search_focus(offset),\n                            }\n                        });\n                        row.col(|ui| {\n                            let off = app.meta_state.meta.bookmarks[idx].offset;\n                            if let Some(region_key) = find_most_specific_region_for_offset(\n                                &app.meta_state.meta.low.regions,\n                                off,\n                            ) {\n                                let region = &app.meta_state.meta.low.regions[region_key];\n                                let ctx_menu = |ui: &mut Ui| {\n                                    region_context_menu(\n                                        ui,\n                                        region,\n                                        region_key,\n                                        &app.meta_state.meta,\n                                        &mut app.cmd,\n                                        &mut gui.cmd,\n                                    );\n                                };\n                                let re = ui.link(&region.name).on_hover_text(&region.desc);\n                                re.context_menu(ctx_menu);\n                                if re.clicked() {\n                                    gui.win.regions.open.set(true);\n                                    gui.win.regions.selected_key = Some(region_key);\n                                }\n                            } else {\n                                ui.label(\"<no region>\");\n                            }\n                        });\n                    });\n                });\n        });\n        if let Some(idx) = self.selected {\n            let Some(mark) = app.meta_state.meta.bookmarks.get_mut(idx) else {\n                per!(\"Invalid bookmark selection: {idx}\");\n                self.selected = None;\n                return;\n            };\n            ui.separator();\n            ui.horizontal(|ui| {\n                if self.edit_name {\n                    let mut out = egui::TextEdit::singleline(&mut mark.label).show(ui);\n                    if out.response.lost_focus() {\n                        self.edit_name = false;\n                    }\n                    if self.focus_text_edit {\n                        out.response.request_focus();\n                        out.state\n                            .cursor\n                            .set_char_range(Some(CCursorRange::select_all(&out.galley)));\n                        out.state.store(ui.ctx(), out.response.id);\n                        self.focus_text_edit = false;\n                    }\n                } else {\n                    ui.heading(&mark.label);\n                }\n                if ui.button(\"✏\").clicked() {\n                    self.edit_name ^= true;\n                    if self.edit_name {\n                        self.focus_text_edit = true;\n                    }\n                }\n                if ui.button(\"⮩\").on_hover_text(\"Jump\").clicked() {\n                    action = Action::Goto(mark.offset);\n                }\n            });\n            ui.horizontal(|ui| {\n                ui.label(\"Offset\");\n                ui.add(egui::DragValue::new(&mut mark.offset));\n                if ui.button(\"👆\").on_hover_text(\"Set to cursor position\").clicked() {\n                    mark.offset = app.edit_state.cursor;\n                }\n            });\n            egui::ComboBox::new(\"type_combo\", \"value type\")\n                .selected_text(mark.value_type.label())\n                .show_ui(ui, |ui| {\n                    macro_rules! int_sel_vals {\n                        ($($t:ident,)*) => {\n                            $(\n                                ui.selectable_value(\n                                    &mut mark.value_type,\n                                    ValueType::$t($t),\n                                    ValueType::$t($t).label(),\n                                );\n                            )*\n                        }\n                    }\n                    ui.selectable_value(\n                        &mut mark.value_type,\n                        ValueType::None,\n                        ValueType::None.label(),\n                    );\n                    int_sel_vals! {\n                        I8, U8,\n                        I16Le, U16Le, I16Be, U16Be,\n                        I32Le, U32Le, I32Be, U32Be,\n                        I64Le, U64Le, I64Be, U64Be,\n                        F32Le, F32Be, F64Le, F64Be,\n                    }\n                    let val = ValueType::StringMap(Default::default());\n                    if ui\n                        .selectable_label(\n                            discriminant(&mark.value_type) == discriminant(&val),\n                            val.label(),\n                        )\n                        .clicked()\n                    {\n                        mark.value_type = val;\n                    }\n                });\n            ui.horizontal(|ui| {\n                ui.label(\"Value\");\n                let value_ui_action = value_ui(\n                    mark,\n                    &mut app.data,\n                    ui,\n                    &mut app.clipboard,\n                    &mut gui.msg_dialog,\n                );\n                match (&value_ui_action, &action) {\n                    (Action::None, Action::None) => {}\n                    (Action::None, Action::Goto(_)) => {}\n                    (Action::Goto(_), Action::None) => action = value_ui_action,\n                    (Action::Goto(_), Action::Goto(_)) => {\n                        msg_fail(\n                            &\"Conflicting goto action\",\n                            \"Ui Action error\",\n                            &mut gui.msg_dialog,\n                        );\n                    }\n                }\n            });\n            #[expect(clippy::single_match, reason = \"Want to add more variants in future\")]\n            match &mut mark.value_type {\n                ValueType::StringMap(list) => {\n                    let text_edit_finished = ui\n                        .add(\n                            egui::TextEdit::singleline(&mut self.value_type_string_buf)\n                                .hint_text(\"key = value\"),\n                        )\n                        .lost_focus()\n                        && ui.input(|inp| inp.key_pressed(egui::Key::Enter));\n                    if text_edit_finished || ui.button(\"Set key = value\").clicked() {\n                        let result = try {\n                            let s = &self.value_type_string_buf;\n                            let (k, v) = s.split_once('=').context(\"Missing `=`\")?;\n                            let k: u8 = k.trim().parse().how()?;\n                            let v = v.trim().to_owned();\n                            list.insert(k, v);\n                        };\n                        msg_if_fail(\n                            result,\n                            \"Failed to set value list kvpair\",\n                            &mut gui.msg_dialog,\n                        );\n                    }\n                }\n                _ => {}\n            }\n            ui.heading(\"Description\");\n            ScrollArea::vertical().id_salt(\"desc_scroll\").max_height(200.0).show(ui, |ui| {\n                ui.add(egui::TextEdit::multiline(&mut mark.desc).code_editor());\n            });\n            if ui.button(\"Delete\").clicked() {\n                app.meta_state.meta.bookmarks.remove(idx);\n                self.selected = None;\n            }\n        }\n        ui.separator();\n        if ui.button(\"Add new at cursor\").clicked() {\n            app.meta_state.meta.bookmarks.push(Bookmark {\n                offset: app.edit_state.cursor,\n                label: format!(\"New bookmark at {}\", app.edit_state.cursor),\n                desc: String::new(),\n                value_type: ValueType::None,\n            });\n            self.selected = Some(app.meta_state.meta.bookmarks.len() - 1);\n        }\n        match action {\n            Action::None => {}\n            Action::Goto(off) => {\n                app.edit_state.cursor = off;\n                app.center_view_on_offset(off);\n                app.hex_ui.flash_cursor();\n            }\n        }\n    }\n\n    fn title(&self) -> &str {\n        \"Bookmarks\"\n    }\n}\n\nfn value_ui(\n    bm: &Bookmark,\n    data: &mut Data,\n    ui: &mut Ui,\n    cb: &mut arboard::Clipboard,\n    msg: &mut MessageDialog,\n) -> Action {\n    macro_rules! val_ui_dispatch {\n        ($i:ident) => {\n            $i.value_ui_for_self(bm, data, ui, cb, msg).to_action()\n        };\n    }\n    match &bm.value_type {\n        ValueType::None => Action::None,\n        ValueType::I8(v) => val_ui_dispatch!(v),\n        ValueType::U8(v) => val_ui_dispatch!(v),\n        ValueType::I16Le(v) => val_ui_dispatch!(v),\n        ValueType::U16Le(v) => val_ui_dispatch!(v),\n        ValueType::I16Be(v) => val_ui_dispatch!(v),\n        ValueType::U16Be(v) => val_ui_dispatch!(v),\n        ValueType::I32Le(v) => val_ui_dispatch!(v),\n        ValueType::U32Le(v) => val_ui_dispatch!(v),\n        ValueType::I32Be(v) => val_ui_dispatch!(v),\n        ValueType::U32Be(v) => val_ui_dispatch!(v),\n        ValueType::I64Le(v) => val_ui_dispatch!(v),\n        ValueType::U64Le(v) => val_ui_dispatch!(v),\n        ValueType::I64Be(v) => val_ui_dispatch!(v),\n        ValueType::U64Be(v) => val_ui_dispatch!(v),\n        ValueType::F32Le(v) => val_ui_dispatch!(v),\n        ValueType::F32Be(v) => val_ui_dispatch!(v),\n        ValueType::F64Le(v) => val_ui_dispatch!(v),\n        ValueType::F64Be(v) => val_ui_dispatch!(v),\n        ValueType::StringMap(v) => val_ui_dispatch!(v),\n    }\n}\n\ntrait ValueTrait: EndianedPrimitive {\n    /// Returns whether the value was changed.\n    fn value_change_ui(\n        &self,\n        ui: &mut Ui,\n        bytes: &mut [u8; Self::BYTE_LEN],\n        cb: &mut arboard::Clipboard,\n        msg: &mut MessageDialog,\n    ) -> ValueUiOutput<Self::Primitive>;\n    fn value_ui_for_self(\n        &self,\n        bm: &Bookmark,\n        data: &mut Data,\n        ui: &mut Ui,\n        cb: &mut arboard::Clipboard,\n        msg: &mut MessageDialog,\n    ) -> UiAction<Self::Primitive>\n    where\n        [(); Self::BYTE_LEN]:,\n    {\n        let range = bm.offset..bm.offset + Self::BYTE_LEN;\n        match data.get_mut(range.clone()) {\n            Some(slice) => {\n                #[expect(\n                    clippy::unwrap_used,\n                    reason = \"If slicing is successful, we're guaranteed to have slice of right length\"\n                )]\n                let out = self.value_change_ui(ui, slice.try_into().unwrap(), cb, msg);\n                if out.changed {\n                    data.widen_dirty_region(DamageRegion::Range(range));\n                }\n                out.action\n            }\n            None => {\n                match data.get(range) {\n                    Some(slice) => {\n                        #[expect(\n                            clippy::unwrap_used,\n                            reason = \"If slicing is successful, we're guaranteed to have slice of right length\"\n                        )]\n                        ui.label(Self::from_bytes(slice.try_into().unwrap()).to_string());\n                    }\n                    None => {\n                        ui.label(\"??\");\n                    }\n                }\n                UiAction::None\n            }\n        }\n    }\n}\n\nstruct ValueUiOutput<T> {\n    changed: bool,\n    action: UiAction<T>,\n}\n\ntrait DefaultUi {}\nimpl DefaultUi for I8 {}\nimpl DefaultUi for U8 {}\nimpl DefaultUi for I16Le {}\nimpl DefaultUi for U16Le {}\nimpl DefaultUi for I16Be {}\nimpl DefaultUi for U16Be {}\nimpl DefaultUi for I32Le {}\nimpl DefaultUi for U32Le {}\nimpl DefaultUi for I32Be {}\nimpl DefaultUi for U32Be {}\nimpl DefaultUi for I64Le {}\nimpl DefaultUi for U64Le {}\nimpl DefaultUi for I64Be {}\nimpl DefaultUi for U64Be {}\nimpl DefaultUi for F32Le {}\nimpl DefaultUi for F32Be {}\nimpl DefaultUi for F64Le {}\nimpl DefaultUi for F64Be {}\n\nimpl<T: EndianedPrimitive + DefaultUi> ValueTrait for T {\n    fn value_change_ui(\n        &self,\n        ui: &mut Ui,\n        bytes: &mut [u8; Self::BYTE_LEN],\n        cb: &mut arboard::Clipboard,\n        msg: &mut MessageDialog,\n    ) -> ValueUiOutput<Self::Primitive> {\n        let mut val = Self::from_bytes(*bytes);\n        let mut action = UiAction::None;\n        let act_mut = &mut action;\n        let ctx_menu = move |ui: &mut Ui| {\n            if ui.button(\"Copy\").clicked() {\n                set_clipboard_string(cb, msg, &val.to_string());\n            }\n            if ui.button(\"Jump\").clicked() {\n                *act_mut = UiAction::Goto(val);\n            }\n        };\n        let re = ui.add(egui::DragValue::new(&mut val));\n        re.context_menu(ctx_menu);\n        let changed = if re.changed() {\n            bytes.copy_from_slice(&Self::to_bytes(val));\n            true\n        } else {\n            false\n        };\n        ValueUiOutput { changed, action }\n    }\n}\n\nimpl EndianedPrimitive for StringMap {\n    type Primitive = u8;\n\n    fn from_bytes(bytes: [u8; Self::BYTE_LEN]) -> Self::Primitive {\n        bytes[0]\n    }\n\n    fn to_bytes(prim: Self::Primitive) -> [u8; Self::BYTE_LEN] {\n        [prim]\n    }\n\n    fn label(&self) -> &'static str {\n        \"string map\"\n    }\n}\n\nimpl ValueTrait for StringMap {\n    fn value_change_ui(\n        &self,\n        ui: &mut Ui,\n        bytes: &mut [u8; Self::BYTE_LEN],\n        _cb: &mut arboard::Clipboard,\n        _msg: &mut MessageDialog,\n    ) -> ValueUiOutput<Self::Primitive> {\n        let val = &mut bytes[0];\n        let mut s = String::new();\n        let label = self.get(val).unwrap_or_else(|| {\n            s = format!(\"[unmapped: {val}]\");\n            &s\n        });\n        let mut changed = false;\n        egui::ComboBox::new(\"val_combo\", \"\").selected_text(label).show_ui(ui, |ui| {\n            for (k, v) in self {\n                if ui.selectable_value(val, *k, v).clicked() {\n                    changed = true;\n                }\n            }\n        });\n        ValueUiOutput {\n            changed,\n            action: UiAction::None,\n        }\n    }\n}\n\nenum Action {\n    None,\n    Goto(usize),\n}\n\nenum UiAction<T> {\n    None,\n    Goto(T),\n}\nimpl<T: AsPrimitive<usize>> UiAction<T> {\n    fn to_action(&self) -> Action {\n        match self {\n            Self::None => Action::None,\n            &Self::Goto(val) => Action::Goto(val.as_()),\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/debug.rs",
    "content": "use {\n    egui::Ui,\n    gamedebug_core::{IMMEDIATE, PERSISTENT},\n};\n\npub fn ui(ui: &mut Ui) {\n    ui.horizontal(|ui| {\n        if ui.button(\"Clear persistent\").clicked() {\n            PERSISTENT.clear();\n        }\n    });\n    ui.separator();\n    egui::ScrollArea::vertical()\n        .max_height(500.)\n        .auto_shrink([false, true])\n        .show(ui, |ui| {\n            IMMEDIATE.for_each(|msg| {\n                ui.label(msg);\n            });\n        });\n    IMMEDIATE.clear();\n    ui.separator();\n    egui::ScrollArea::vertical()\n        .id_salt(\"per_scroll\")\n        .max_height(500.0)\n        .show(ui, |ui| {\n            egui::Grid::new(\"per_grid\").striped(true).show(ui, |ui| {\n                PERSISTENT.for_each(|msg| {\n                    ui.label(\n                        egui::RichText::new(msg.frame.to_string()).color(egui::Color32::DARK_GRAY),\n                    );\n                    if let Some(src_loc) = &msg.src_loc {\n                        let txt = format!(\"{}:{}:{}\", src_loc.file, src_loc.line, src_loc.column);\n                        if ui.link(&txt).on_hover_text(\"Click to copy to clipboard\").clicked() {\n                            ui.ctx().copy_text(txt);\n                        }\n                    }\n                    ui.label(&msg.info);\n                    ui.end_row();\n                });\n            });\n        });\n}\n"
  },
  {
    "path": "src/gui/windows/external_command.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        result_ext::AnyhowConv as _,\n        shell::{msg_fail, msg_if_fail},\n        str_ext::StrExt as _,\n    },\n    anyhow::Context as _,\n    core::f32,\n    std::{\n        ffi::OsString,\n        io::Read as _,\n        path::PathBuf,\n        process::{Child, Command, ExitStatus, Stdio},\n    },\n};\n\npub struct ExternalCommandWindow {\n    pub open: WindowOpen,\n    cmd_str: String,\n    child: Option<Child>,\n    exit_status: Option<ExitStatus>,\n    err_msg: String,\n    stdout: String,\n    stderr: String,\n    auto_exec: bool,\n    inherited_streams: bool,\n    selection_only: bool,\n    temp_file_name: String,\n    working_dir: WorkingDir,\n}\n\n#[derive(PartialEq)]\nenum WorkingDir {\n    /// Create a temporary directory for executing the command\n    Temp,\n    /// Execute in the same directory as Hexerator's working dir\n    Hexerator,\n    /// Execute in the same directory as the opened document\n    Document,\n}\n\nimpl WorkingDir {\n    fn label(&self) -> &'static str {\n        match self {\n            Self::Temp => \"Temp\",\n            Self::Hexerator => \"Hexerator\",\n            Self::Document => \"Document\",\n        }\n    }\n}\n\nimpl Default for ExternalCommandWindow {\n    fn default() -> Self {\n        Self {\n            open: Default::default(),\n            cmd_str: Default::default(),\n            child: Default::default(),\n            exit_status: Default::default(),\n            err_msg: Default::default(),\n            stdout: Default::default(),\n            stderr: Default::default(),\n            auto_exec: Default::default(),\n            inherited_streams: Default::default(),\n            selection_only: true,\n            temp_file_name: String::from(\"hexerator_data_tmp.bin\"),\n            working_dir: WorkingDir::Temp,\n        }\n    }\n}\n\nenum Arg<'src> {\n    TmpFilePath,\n    Custom(&'src str),\n}\n\nimpl super::Window for ExternalCommandWindow {\n    fn ui(&mut self, WinCtx { ui, gui, app, .. }: WinCtx) {\n        let re = ui.add(\n            egui::TextEdit::multiline(&mut self.cmd_str)\n                .hint_text(\"Use {} to substitute filename.\\nExample: aplay {} -f s16_le\")\n                .desired_width(f32::INFINITY),\n        );\n        if self.open.just_now() {\n            re.request_focus();\n        }\n        ui.horizontal(|ui| {\n            egui::ComboBox::new(\"wd_cb\", \"Working dir\")\n                .selected_text(self.working_dir.label())\n                .show_ui(ui, |ui| {\n                    ui.selectable_value(\n                        &mut self.working_dir,\n                        WorkingDir::Temp,\n                        WorkingDir::Temp.label(),\n                    );\n                    ui.selectable_value(\n                        &mut self.working_dir,\n                        WorkingDir::Document,\n                        WorkingDir::Document.label(),\n                    );\n                    ui.selectable_value(\n                        &mut self.working_dir,\n                        WorkingDir::Hexerator,\n                        WorkingDir::Hexerator.label(),\n                    );\n                });\n            if let WorkingDir::Temp = self.working_dir {\n                ui.label(\"Temp file name\");\n                ui.text_edit_singleline(&mut self.temp_file_name);\n            }\n        });\n        ui.horizontal(|ui| {\n            ui.add_enabled(\n                app.hex_ui.selection().is_some() && self.working_dir == WorkingDir::Temp,\n                egui::Checkbox::new(&mut self.selection_only, \"Selection only\"),\n            );\n            ui.checkbox(&mut self.inherited_streams, \"Inherited stdout/stderr\")\n                .on_hover_text(\n                    \"Use this for large amounts of data that could block child processes, like music players, etc.\"\n                );\n        });\n        let exec_enabled = self.child.is_none() && !self.temp_file_name.is_empty_or_ws_only();\n        if ui.input(|inp| inp.key_pressed(egui::Key::Escape)) {\n            self.open.set(false);\n        }\n        ui.horizontal(|ui| {\n            if ui.add_enabled(exec_enabled, egui::Button::new(\"Execute (ctrl+E)\")).clicked()\n                || (exec_enabled\n                    && ((ui.input(|inp| {\n                        inp.key_pressed(egui::Key::E) && inp.modifiers.ctrl && !self.open.just_now()\n                    })) || self.auto_exec))\n            {\n                let res = try {\n                    // Parse args\n                    let (cmd, args) = parse(&self.cmd_str)?;\n                    // Generate temp file\n                    let range = if self.selection_only\n                        && let Some(sel) = app.hex_ui.selection()\n                    {\n                        sel.begin..=sel.end\n                    } else {\n                        0..=app.data.len() - 1\n                    };\n                    let dir: PathBuf;\n                    let file_path: PathBuf;\n                    match self.working_dir {\n                        WorkingDir::Temp => {\n                            dir = std::env::temp_dir();\n                            let path = dir.join(&self.temp_file_name);\n                            let data = app.data.get(range).context(\"Range out of bounds\")?;\n                            std::fs::write(&path, data).how()?;\n                            file_path = path;\n                        }\n                        WorkingDir::Hexerator => {\n                            dir = std::env::current_dir().how()?;\n                            file_path = dir.clone();\n                        }\n                        WorkingDir::Document => match &app.src_args.file {\n                            Some(path) => {\n                                dir = path\n                                    .parent()\n                                    .context(\"Document path has no parent\")?\n                                    .to_path_buf();\n                                file_path = dir.clone();\n                            }\n                            None => {\n                                do yeet anyhow::anyhow!(\"Document has no path\");\n                            }\n                        },\n                    }\n\n                    // Spawn process\n                    let mut cmd = Command::new(cmd);\n                    cmd.current_dir(&dir).args(resolve_args(args, &file_path));\n                    if self.inherited_streams {\n                        cmd.stdout(Stdio::inherit());\n                        cmd.stderr(Stdio::inherit());\n                    } else {\n                        cmd.stdout(Stdio::piped());\n                        cmd.stderr(Stdio::piped());\n                    }\n                    let handle = cmd.spawn().how()?;\n                    self.child = Some(handle);\n                    // Clear output from previous run\n                    self.stderr.clear();\n                    self.stdout.clear();\n                };\n                if let Err(e) = res {\n                    msg_fail(&e, \"Failed to spawn command\", &mut gui.msg_dialog);\n                    self.auto_exec = false;\n                }\n            }\n            ui.checkbox(&mut self.auto_exec, \"Auto execute\")\n                .on_hover_text(\"Execute again after process finishes\");\n        });\n\n        if let Some(child) = &mut self.child {\n            ui.horizontal(|ui| {\n                ui.spinner();\n                ui.label(format!(\"{} running\", child.id()));\n                if ui.button(\"Kill\").clicked() {\n                    self.auto_exec = false;\n                    msg_if_fail(child.kill(), \"Failed to kill child\", &mut gui.msg_dialog);\n                }\n            });\n            match child.try_wait() {\n                Ok(opt_status) => {\n                    if let Some(status) = opt_status {\n                        if let Some(stdout) = &mut child.stdout {\n                            self.stdout.clear();\n                            if let Err(e) = stdout.read_to_string(&mut self.stdout) {\n                                self.stdout = format!(\"<Error reading stdout: {e}>\");\n                            }\n                        }\n                        if let Some(stderr) = &mut child.stderr {\n                            self.stderr.clear();\n                            if let Err(e) = stderr.read_to_string(&mut self.stderr) {\n                                self.stderr = format!(\"<Error reading stderr: {e}>\");\n                            }\n                        }\n                        self.child = None;\n                        self.exit_status = Some(status);\n                    }\n                }\n                Err(e) => self.err_msg = e.to_string(),\n            }\n        }\n        if !self.err_msg.is_empty() {\n            ui.label(egui::RichText::new(&self.err_msg).color(egui::Color32::RED));\n        }\n        if !self.stdout.is_empty() {\n            ui.label(\"stdout\");\n            egui::ScrollArea::vertical()\n                .id_salt(\"stdout\")\n                .auto_shrink([false, true])\n                .max_height(200.0)\n                .show(ui, |ui| {\n                    ui.text_edit_multiline(&mut &self.stdout[..]);\n                });\n        }\n        if !self.stderr.is_empty() {\n            ui.label(\"stderr\");\n            egui::ScrollArea::vertical()\n                .id_salt(\"stderr\")\n                .auto_shrink([false, true])\n                .max_height(200.0)\n                .show(ui, |ui| {\n                    ui.text_edit_multiline(&mut &self.stderr[..]);\n                });\n        }\n    }\n\n    fn title(&self) -> &str {\n        \"External command\"\n    }\n}\n\nfn resolve_args<'src>(\n    args: impl Iterator<Item = Arg<'src>> + 'src,\n    path: &'src PathBuf,\n) -> impl Iterator<Item = OsString> + 'src {\n    args.map(|arg| match arg {\n        Arg::TmpFilePath => path.into(),\n        Arg::Custom(c) => c.into(),\n    })\n}\n\nfn parse(input: &'_ str) -> anyhow::Result<(&'_ str, impl Iterator<Item = Arg<'_>>)> {\n    let mut tokens = input.split_whitespace();\n    let cmd = tokens.next().context(\"Missing command\")?;\n    let iter = tokens.map(|tok| {\n        if tok == \"{}\" {\n            Arg::TmpFilePath\n        } else {\n            Arg::Custom(tok)\n        }\n    });\n    Ok((cmd, iter))\n}\n"
  },
  {
    "path": "src/gui/windows/file_diff_result.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        app::read_source_to_buf,\n        gui::windows::regions::region_context_menu,\n        meta::{Meta, RegionKey, find_most_specific_region_for_offset},\n        shell::msg_if_fail,\n    },\n    egui_extras::Column,\n    std::{path::PathBuf, time::Instant},\n};\n\npub struct FileDiffResultWindow {\n    pub file_data: Vec<u8>,\n    pub offsets: Vec<usize>,\n    pub open: WindowOpen,\n    pub path: PathBuf,\n    pub auto_refresh: bool,\n    pub auto_refresh_interval_ms: u32,\n    pub last_refresh: Instant,\n    /// Allows filtering differences based on diff threshold\n    pub diff_threshold: u8,\n    /// Display the relative offset values as relative to this value\n    pub base_offset: usize,\n}\n\nimpl Default for FileDiffResultWindow {\n    fn default() -> Self {\n        Self {\n            offsets: Default::default(),\n            open: Default::default(),\n            path: Default::default(),\n            auto_refresh: Default::default(),\n            auto_refresh_interval_ms: Default::default(),\n            last_refresh: Instant::now(),\n            file_data: Vec::new(),\n            diff_threshold: 1,\n            base_offset: 0,\n        }\n    }\n}\nimpl super::Window for FileDiffResultWindow {\n    fn ui(\n        &mut self,\n        WinCtx {\n            ui,\n            gui,\n            app,\n            font_size,\n            line_spacing,\n            ..\n        }: WinCtx,\n    ) {\n        let mut action = Action::None;\n        ui.horizontal(|ui| {\n            if let Some(src_file) = &app.src_args.file {\n                ui.colored_label(egui::Color32::GREEN, src_file.display().to_string());\n            }\n            ui.label(\"vs\");\n            ui.colored_label(egui::Color32::RED, self.path.display().to_string());\n        });\n        ui.horizontal(|ui| {\n            if ui\n                .button(\"🔁 Switch\")\n                .on_hover_text(\"Switch to the diffed against file (keeping the current meta)\")\n                .clicked()\n            {\n                let prev_pref = app.preferences.keep_meta;\n                let prev_path = app.src_args.file.clone();\n                app.preferences.keep_meta = true;\n                app.load_file(\n                    self.path.clone(),\n                    false,\n                    &mut gui.msg_dialog,\n                    font_size,\n                    line_spacing,\n                );\n                app.preferences.keep_meta = prev_pref;\n                if let Some(path) = prev_path {\n                    msg_if_fail(\n                        app.diff_with_file(path, self),\n                        \"Failed to diff\",\n                        &mut gui.msg_dialog,\n                    );\n                }\n            }\n            if ui.button(\"🖹 Diff with...\").on_hover_text(\"Diff with another file\").clicked() {\n                gui.fileops.diff_with_file(app.source_file());\n            }\n        });\n        ui.separator();\n        ui.horizontal(|ui| {\n            if ui\n                .button(\"Filter unchanged\")\n                .on_hover_text(\"Keep only the unchanged values\")\n                .clicked()\n            {\n                let result = try {\n                    let file_data = read_source_to_buf(&self.path, &app.src_args)?;\n                    self.offsets.retain(|&offs| self.file_data[offs] == file_data[offs]);\n                };\n                msg_if_fail(result, \"Filter unchanged failed\", &mut gui.msg_dialog);\n            }\n            if ui\n                .button(\"Filter changed\")\n                .on_hover_text(\"Keep only the values that changed\")\n                .clicked()\n            {\n                let result = try {\n                    let file_data = read_source_to_buf(&self.path, &app.src_args)?;\n                    self.offsets.retain(|&offs| self.file_data[offs] != file_data[offs]);\n                };\n                msg_if_fail(result, \"Filter unchanged failed\", &mut gui.msg_dialog);\n            }\n            if ui\n                .button(\"Filter diff>threshold\")\n                .on_hover_text(\n                    \"Keep only the values whose difference is larger than the provided threshold\",\n                )\n                .clicked()\n            {\n                self.offsets.retain(|&offs| {\n                    self.file_data[offs].abs_diff(app.data[offs]) > self.diff_threshold\n                });\n            }\n            ui.add(egui::DragValue::new(&mut self.diff_threshold));\n        });\n        ui.horizontal(|ui| {\n            if ui.button(\"Refresh\").clicked()\n                || (self.auto_refresh\n                    && self.last_refresh.elapsed().as_millis()\n                        >= u128::from(self.auto_refresh_interval_ms))\n            {\n                self.last_refresh = Instant::now();\n                let result = try {\n                    self.file_data = read_source_to_buf(&self.path, &app.src_args)?;\n                };\n                msg_if_fail(result, \"Refresh failed\", &mut gui.msg_dialog);\n            }\n            ui.checkbox(&mut self.auto_refresh, \"Auto refresh\");\n            ui.label(\"Interval\");\n            ui.add(egui::DragValue::new(&mut self.auto_refresh_interval_ms));\n            if ui.link(\"Base offset\").clicked() {\n                action = Action::Goto(self.base_offset);\n            }\n            ui.add(egui::DragValue::new(&mut self.base_offset));\n            if ui.button(\"Set to cursor\").clicked() {\n                self.base_offset = app.edit_state.cursor;\n            }\n        });\n        ui.separator();\n        if self.offsets.is_empty() {\n            ui.label(\"No difference\");\n            return;\n        } else {\n            ui.horizontal(|ui| {\n                ui.label(format!(\"{} bytes different.\", self.offsets.len()));\n                if ui.button(\"Highlight all\").clicked() {\n                    gui.highlight_set.clear();\n                    for &offs in &self.offsets {\n                        gui.highlight_set.insert(offs);\n                        if let Some((_, bm)) =\n                            Meta::bookmark_for_offset(&app.meta_state.meta.bookmarks, offs)\n                        {\n                            for i in 1..bm.value_type.byte_len() {\n                                gui.highlight_set.insert(offs + i);\n                            }\n                        }\n                    }\n                }\n                #[expect(clippy::collapsible_if)]\n                if !gui.highlight_set.is_empty() {\n                    if ui.button(\"Clear highlight\").clicked() {\n                        gui.highlight_set.clear();\n                    }\n                }\n            });\n            ui.separator();\n        }\n        egui_extras::TableBuilder::new(ui)\n            .columns(Column::auto(), 4)\n            .column(Column::remainder())\n            .resizable(true)\n            .striped(true)\n            .header(32.0, |mut row| {\n                row.col(|ui| {\n                    ui.colored_label(egui::Color32::GREEN, \"My value\");\n                });\n                row.col(|ui| {\n                    ui.colored_label(egui::Color32::RED, \"File value\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Offset\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Region\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Bookmark\");\n                });\n            })\n            .body(|body| {\n                body.rows(20.0, self.offsets.len(), |mut row| {\n                    let offs = self.offsets[row.index()];\n                    let bm = Meta::bookmark_for_offset(&app.meta_state.meta.bookmarks, offs)\n                        .map(|(_, bm)| bm);\n                    row.col(|ui| {\n                        let s = match bm {\n                            Some(bm) => bm\n                                .value_type\n                                .read(&app.data[offs..])\n                                .map_or(\"err\".into(), |v| v.to_string()),\n                            None => app.data[offs].to_string(),\n                        };\n                        ui.label(s);\n                    });\n                    row.col(|ui| {\n                        let s = match bm {\n                            Some(bm) => bm\n                                .value_type\n                                .read(&self.file_data[offs..])\n                                .map_or(\"err\".into(), |v| v.to_string()),\n                            None => self.file_data[offs].to_string(),\n                        };\n                        ui.label(s);\n                    });\n                    row.col(|ui| {\n                        #[expect(clippy::cast_possible_wrap)]\n                        let display_offs: isize = offs as isize - self.base_offset as isize;\n                        let re = ui.link(display_offs.to_string());\n                        re.context_menu(|ui| {\n                            if ui.button(\"Add bookmark\").clicked() {\n                                crate::gui::add_new_bookmark(app, gui, offs);\n                            }\n                        });\n                        if re.clicked() {\n                            action = Action::Goto(offs);\n                        }\n                    });\n                    row.col(|ui| {\n                        match find_most_specific_region_for_offset(\n                            &app.meta_state.meta.low.regions,\n                            offs,\n                        ) {\n                            Some(reg_key) => {\n                                let reg = &app.meta_state.meta.low.regions[reg_key];\n                                ui.menu_button(&reg.name, |ui| {\n                                    if ui.button(\"Remove region from results\").clicked() {\n                                        action = Action::RemoveRegion(reg_key);\n                                    }\n                                })\n                                .response\n                                .context_menu(|ui| {\n                                    region_context_menu(\n                                        ui,\n                                        reg,\n                                        reg_key,\n                                        &app.meta_state.meta,\n                                        &mut app.cmd,\n                                        &mut gui.cmd,\n                                    );\n                                });\n                            }\n                            None => {\n                                ui.label(\"[no region]\");\n                            }\n                        }\n                    });\n                    row.col(|ui| {\n                        match app\n                            .meta_state\n                            .meta\n                            .bookmarks\n                            .iter()\n                            .enumerate()\n                            .find(|(_i, b)| b.offset == offs)\n                        {\n                            Some((idx, bookmark)) => {\n                                if ui.link(&bookmark.label).on_hover_text(&bookmark.desc).clicked()\n                                {\n                                    gui.win.bookmarks.open.set(true);\n                                    gui.win.bookmarks.selected = Some(idx);\n                                }\n                            }\n                            None => {\n                                ui.label(\"-\");\n                            }\n                        }\n                    });\n                });\n            });\n        match action {\n            Action::None => {}\n            Action::Goto(off) => {\n                app.center_view_on_offset(off);\n                app.edit_state.set_cursor(off);\n                app.hex_ui.flash_cursor();\n            }\n            Action::RemoveRegion(key) => self.offsets.retain(|&offs| {\n                let reg =\n                    find_most_specific_region_for_offset(&app.meta_state.meta.low.regions, offs);\n                reg != Some(key)\n            }),\n        }\n    }\n\n    fn title(&self) -> &str {\n        \"File Diff results\"\n    }\n}\n\nenum Action {\n    None,\n    Goto(usize),\n    RemoveRegion(RegionKey),\n}\n"
  },
  {
    "path": "src/gui/windows/find_dialog.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        app::{get_clipboard_string, set_clipboard_string},\n        damage_region::DamageRegion,\n        gui::{\n            message_dialog::{Icon, MessageDialog},\n            windows::region_context_menu,\n        },\n        hex_ui::HexUi,\n        meta::{\n            Bookmark, Meta, find_most_specific_region_for_offset,\n            region::Region,\n            value_type::{\n                EndianedPrimitive, F32Be, F32Le, F64Be, F64Le, I8, I16Be, I16Le, I32Be, I32Le,\n                I64Be, I64Le, U8, U16Be, U16Le, U32Be, U32Le, U64Be, U64Le, ValueType,\n            },\n        },\n        parse_radix::parse_guess_radix,\n        shell::{msg_fail, msg_if_fail},\n    },\n    egui::{Align, Ui},\n    egui_extras::{Column, Size, StripBuilder, TableBuilder},\n    itertools::Itertools as _,\n    std::{collections::HashMap, error::Error, str::FromStr},\n    strum::{EnumIter, IntoEnumIterator as _, IntoStaticStr},\n};\n\n#[derive(Default, Debug, PartialEq, Eq, EnumIter, IntoStaticStr)]\npub enum FindType {\n    I8,\n    #[default]\n    U8,\n    I16Le,\n    I16Be,\n    U16Le,\n    U16Be,\n    I32Le,\n    I32Be,\n    U32Le,\n    U32Be,\n    I64Le,\n    I64Be,\n    U64Le,\n    U64Be,\n    F32Le,\n    F32Be,\n    F64Le,\n    F64Be,\n    Ascii,\n    StringDiff,\n    /// Equivalence pattern\n    EqPattern,\n    HexString,\n}\n\nimpl FindType {\n    fn to_value_type(&self) -> ValueType {\n        match self {\n            Self::I8 => ValueType::I8(I8),\n            Self::U8 => ValueType::U8(U8),\n            Self::I16Le => ValueType::I16Le(I16Le),\n            Self::I16Be => ValueType::I16Be(I16Be),\n            Self::U16Le => ValueType::U16Le(U16Le),\n            Self::U16Be => ValueType::U16Be(U16Be),\n            Self::I32Le => ValueType::I32Le(I32Le),\n            Self::I32Be => ValueType::I32Be(I32Be),\n            Self::U32Le => ValueType::U32Le(U32Le),\n            Self::U32Be => ValueType::U32Be(U32Be),\n            Self::I64Le => ValueType::I64Le(I64Le),\n            Self::I64Be => ValueType::I64Be(I64Be),\n            Self::U64Le => ValueType::U64Le(U64Le),\n            Self::U64Be => ValueType::U64Be(U64Be),\n            Self::F32Le => ValueType::F32Le(F32Le),\n            Self::F32Be => ValueType::F32Be(F32Be),\n            Self::F64Le => ValueType::F64Le(F64Le),\n            Self::F64Be => ValueType::F64Be(F64Be),\n            Self::Ascii => ValueType::None,\n            Self::StringDiff => ValueType::None,\n            Self::EqPattern => ValueType::None,\n            Self::HexString => ValueType::U8(U8),\n        }\n    }\n    fn help_str(&self) -> &'static str {\n        match self {\n            Self::I8 => \"signed 8 bit integer\",\n            Self::U8 => \"unsigned 8 bit integer\",\n            Self::I16Le => \"signed 16 bit integer (little endian)\",\n            Self::I16Be => \"signed 16 bit integer (big endian)\",\n            Self::U16Le => \"unsigned 16 bit integer (little endian)\",\n            Self::U16Be => \"unsigned 16 bit integer (big endian)\",\n            Self::I32Le => \"signed 32 bit integer (little endian)\",\n            Self::I32Be => \"signed 32 bit integer (big endian)\",\n            Self::U32Le => \"unsigned 32 bit integer (little endian)\",\n            Self::U32Be => \"unsigned 32 bit integer (big endian)\",\n            Self::I64Le => \"signed 64 bit integer (little endian)\",\n            Self::I64Be => \"signed 64 bit integer (big endian)\",\n            Self::U64Le => \"unsigned 64 bit integer (little endian)\",\n            Self::U64Be => \"unsigned 64 bit integer (big endian)\",\n            Self::F32Le => \"32 bit float (little endian)\",\n            Self::F32Be => \"32 bit float (big endian)\",\n            Self::F64Le => \"64 bit float (little endian)\",\n            Self::F64Be => \"64 bit float (big endian)\",\n            Self::Ascii => \"Ascii string\",\n            Self::StringDiff => {\n                \"Searches the string difference pattern of your ascii input\n\nUseful to find alphabetic data in non-ascii character encodings.\n\nTakes advantage of the fact that A-Z and 0-9, etc, are usually\nnext to each other regardless of the encoding.\"\n            }\n            Self::EqPattern => {\n                \"Searches the byte equivalence pattern of your ascii input\nFor example if you type `aabbca`\nthen it will match inputs like `00 00 FF FF CC 00`\"\n            }\n            Self::HexString => \"Search a hex string e.g. `ff 00 ff`\",\n        }\n    }\n}\n\n#[derive(Default)]\npub struct FindDialog {\n    pub open: WindowOpen,\n    pub find_input: String,\n    pub replace_input: String,\n    /// Results, as a Vec that can be indexed. Needed because of search cursor.\n    pub results_vec: Vec<usize>,\n    /// Used to keep track of previous/next result to go to\n    pub result_cursor: usize,\n    /// When Some, the results list should be scrolled to the offset of that result\n    pub scroll_to: Option<usize>,\n    pub filter_results: bool,\n    pub rapid_eq_filter: bool,\n    pub find_type: FindType,\n    /// Used for increased/decreased unknown value search\n    pub data_snapshot: Vec<u8>,\n    /// Reload the source before search\n    pub reload_before_search: bool,\n    /// Only search in selection\n    pub selection_only: bool,\n}\n\nimpl super::Window for FindDialog {\n    fn ui(&mut self, WinCtx { ui, gui, app, .. }: WinCtx) {\n        ui.horizontal(|ui| {\n            let re = egui::ComboBox::new(\"type_combo\", \"Data type\")\n                .selected_text(<&str>::from(&self.find_type))\n                .show_ui(ui, |ui| {\n                    for type_ in FindType::iter() {\n                        let label = <&str>::from(&type_);\n                        let help = type_.help_str();\n                        let re = ui.selectable_value(&mut self.find_type, type_, label);\n                        re.on_hover_text(help);\n                    }\n                });\n            re.response.on_hover_text(self.find_type.help_str());\n            ui.checkbox(&mut self.reload_before_search, \"Reload\")\n                .on_hover_text(\"Reload source before every search\");\n            ui.checkbox(&mut self.selection_only, \"Selection only\")\n                .on_hover_text(\"Only search in selection\");\n        });\n        let re = ui.add(egui::TextEdit::singleline(&mut self.find_input).hint_text(\"🔍 Find\"));\n        if self.open.just_now() {\n            re.request_focus();\n        }\n        if re.lost_focus() && ui.input(|inp| inp.key_pressed(egui::Key::Enter)) {\n            if self.reload_before_search {\n                self.reload_data(app, gui);\n            }\n            let (data, offs) = self.data_to_search(app);\n            msg_if_fail(\n                do_search(data, offs, self, gui),\n                \"Search failed\",\n                &mut gui.msg_dialog,\n            );\n            if let Some(&off) = self.results_vec.first() {\n                app.search_focus(off);\n            }\n        }\n        if self.find_type == FindType::Ascii || self.find_type == FindType::HexString {\n            ui.horizontal(|ui| {\n                ui.add(egui::TextEdit::singleline(&mut self.replace_input).hint_text(\"🔁 Replace\"));\n                if ui\n                    .add_enabled(\n                        !self.results_vec.is_empty(),\n                        egui::Button::new(\"Replace all\"),\n                    )\n                    .clicked()\n                {\n                    let bytes_buf;\n                    let replace_data = if self.find_type == FindType::Ascii {\n                        self.replace_input.as_bytes()\n                    } else {\n                        match crate::find_util::parse_hex_string(&self.replace_input) {\n                            Ok(bytes) => {\n                                bytes_buf = bytes;\n                                &bytes_buf\n                            }\n                            Err(e) => {\n                                msg_fail(&e, \"Failed to parse hex string\", &mut gui.msg_dialog);\n                                return;\n                            }\n                        }\n                    };\n                    for &offset in &self.results_vec {\n                        app.data[offset..offset + replace_data.len()].copy_from_slice(replace_data);\n                    }\n                }\n            });\n        }\n        ui.horizontal(|ui| {\n            ui.checkbox(&mut self.filter_results, \"Filter results\")\n                .on_hover_text(\"Base search on existing results\");\n            ui.add_enabled(\n                self.filter_results,\n                egui::Checkbox::new(&mut self.rapid_eq_filter, \"Rapid '=' filter\"),\n            )\n            .on_hover_text(\"Filter every frame for data that hasn't changed\");\n        });\n        if self.rapid_eq_filter {\n            if self.reload_before_search {\n                self.reload_data(app, gui);\n            }\n            let (data, offset) = self.data_to_search(app);\n            eq_filter(self, data, offset);\n        }\n        StripBuilder::new(ui)\n            .size(Size::initial(400.0))\n            .size(Size::exact(20.0))\n            .size(Size::exact(20.0))\n            .vertical(|mut strip| {\n                strip.cell(|ui| {\n                    ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n                    let mut action = Action::None;\n                    TableBuilder::new(ui)\n                        .striped(true)\n                        .columns(Column::auto(), 3)\n                        .column(Column::remainder())\n                        .resizable(true)\n                        .header(16.0, |mut row| {\n                            row.col(|ui| {\n                                ui.label(\"Offset\");\n                            });\n                            row.col(|ui| {\n                                ui.label(\"Value\");\n                            });\n                            row.col(|ui| {\n                                ui.label(\"Region\");\n                            });\n                            row.col(|ui| {\n                                ui.label(\"Bookmark\");\n                            });\n                        })\n                        .body(|body| {\n                            body.rows(20.0, self.results_vec.len(), |mut row| {\n                                let i = row.index();\n                                let off = self.results_vec[i];\n                                let (_, col1_re) = row.col(|ui| {\n                                    let re = ui\n                                        .selectable_label(self.result_cursor == i, off.to_string());\n                                    re.context_menu(|ui| {\n                                        if ui.button(\"Remove from results\").clicked() {\n                                            action = Action::RemoveIdxFromResults(i);\n                                        }\n                                    });\n                                    if re.clicked() {\n                                        app.search_focus(off);\n                                        self.result_cursor = i;\n                                    }\n                                });\n                                row.col(|ui| {\n                                    let damage = match self.find_type {\n                                        FindType::I8 => {\n                                            data_value_label::<I8>(ui, &mut app.data, off)\n                                        }\n                                        FindType::U8 => {\n                                            data_value_label::<U8>(ui, &mut app.data, off)\n                                        }\n                                        FindType::I16Le => {\n                                            data_value_label::<I16Le>(ui, &mut app.data, off)\n                                        }\n                                        FindType::I16Be => {\n                                            data_value_label::<I16Be>(ui, &mut app.data, off)\n                                        }\n                                        FindType::U16Le => {\n                                            data_value_label::<U16Le>(ui, &mut app.data, off)\n                                        }\n                                        FindType::U16Be => {\n                                            data_value_label::<U16Be>(ui, &mut app.data, off)\n                                        }\n                                        FindType::I32Le => {\n                                            data_value_label::<I32Le>(ui, &mut app.data, off)\n                                        }\n                                        FindType::I32Be => {\n                                            data_value_label::<I32Be>(ui, &mut app.data, off)\n                                        }\n                                        FindType::U32Le => {\n                                            data_value_label::<U32Le>(ui, &mut app.data, off)\n                                        }\n                                        FindType::U32Be => {\n                                            data_value_label::<U32Be>(ui, &mut app.data, off)\n                                        }\n                                        FindType::I64Le => {\n                                            data_value_label::<I64Le>(ui, &mut app.data, off)\n                                        }\n                                        FindType::I64Be => {\n                                            data_value_label::<I64Be>(ui, &mut app.data, off)\n                                        }\n                                        FindType::U64Le => {\n                                            data_value_label::<U64Le>(ui, &mut app.data, off)\n                                        }\n                                        FindType::U64Be => {\n                                            data_value_label::<U64Be>(ui, &mut app.data, off)\n                                        }\n                                        FindType::F32Le => {\n                                            data_value_label::<F32Le>(ui, &mut app.data, off)\n                                        }\n                                        FindType::F32Be => {\n                                            data_value_label::<F32Be>(ui, &mut app.data, off)\n                                        }\n                                        FindType::F64Le => {\n                                            data_value_label::<F64Le>(ui, &mut app.data, off)\n                                        }\n                                        FindType::F64Be => {\n                                            data_value_label::<F64Be>(ui, &mut app.data, off)\n                                        }\n                                        FindType::Ascii => {\n                                            data_value_label::<U8>(ui, &mut app.data, off)\n                                        }\n                                        FindType::HexString => {\n                                            data_value_label::<U8>(ui, &mut app.data, off)\n                                        }\n                                        FindType::StringDiff => {\n                                            data_value_label::<U8>(ui, &mut app.data, off)\n                                        }\n                                        FindType::EqPattern => {\n                                            data_value_label::<U8>(ui, &mut app.data, off)\n                                        }\n                                    };\n                                    if let Some(damage) = damage {\n                                        app.data.widen_dirty_region(damage);\n                                    }\n                                });\n                                row.col(|ui| {\n                                    match find_most_specific_region_for_offset(\n                                        &app.meta_state.meta.low.regions,\n                                        off,\n                                    ) {\n                                        Some(key) => {\n                                            let reg = &app.meta_state.meta.low.regions[key];\n                                            let ctx_menu = |ui: &mut Ui| {\n                                                region_context_menu(\n                                                    ui,\n                                                    reg,\n                                                    key,\n                                                    &app.meta_state.meta,\n                                                    &mut app.cmd,\n                                                    &mut gui.cmd,\n                                                );\n                                                ui.separator();\n                                                if ui.button(\"Remove region from results\").clicked()\n                                                {\n                                                    action = Action::RemoveRegionFromResults(key);\n                                                }\n                                            };\n                                            let re = ui.link(&reg.name);\n                                            re.context_menu(ctx_menu);\n                                            if re.clicked() {\n                                                gui.win.regions.open.set(true);\n                                                gui.win.regions.selected_key = Some(key);\n                                            }\n                                        }\n                                        None => {\n                                            ui.label(\"[no region]\");\n                                        }\n                                    }\n                                });\n                                row.col(|ui| {\n                                    match Meta::bookmark_for_offset(\n                                        &app.meta_state.meta.bookmarks,\n                                        off,\n                                    ) {\n                                        Some((bm_idx, bm)) => {\n                                            if ui.link(&bm.label).on_hover_text(&bm.desc).clicked()\n                                            {\n                                                gui.win.bookmarks.open.set(true);\n                                                gui.win.bookmarks.selected = Some(bm_idx);\n                                            }\n                                        }\n                                        None => {\n                                            if ui\n                                                .button(\"✚\")\n                                                .on_hover_text(\"Add new bookmark\")\n                                                .clicked()\n                                            {\n                                                let idx = app.meta_state.meta.bookmarks.len();\n                                                app.meta_state.meta.bookmarks.push(Bookmark {\n                                                    offset: off,\n                                                    label: \"New bookmark\".into(),\n                                                    desc: String::new(),\n                                                    value_type: self.find_type.to_value_type(),\n                                                });\n                                                gui.win.bookmarks.open.set(true);\n                                                gui.win.bookmarks.selected = Some(idx);\n                                                gui.win.bookmarks.edit_name = true;\n                                                gui.win.bookmarks.focus_text_edit = true;\n                                            }\n                                        }\n                                    }\n                                });\n                                if let Some(scroll_off) = self.scroll_to\n                                    && scroll_off == i\n                                {\n                                    // We use center align, because it keeps the selected element in\n                                    // view at all times, preventing the issue of it becoming out\n                                    // of view, and scroll_to_me not being called because of that.\n                                    col1_re.scroll_to_me(Some(Align::Center));\n                                    self.scroll_to = None;\n                                }\n                            });\n                        });\n                    match action {\n                        Action::None => {}\n                        Action::RemoveRegionFromResults(key) => {\n                            let reg = &app.meta_state.meta.low.regions[key];\n                            self.results_vec.retain(|&idx| !reg.region.contains(idx));\n                        }\n                        Action::RemoveIdxFromResults(idx) => {\n                            self.results_vec.remove(idx);\n                        }\n                    }\n                });\n                strip.cell(|ui| {\n                    ui.horizontal(|ui| {\n                        if self.results_vec.is_empty() {\n                            ui.disable();\n                        }\n                        if (ui.button(\"Previous (P)\").clicked()\n                            || ui.input(|inp| inp.key_pressed(egui::Key::P)))\n                            && self.result_cursor > 0\n                            && !self.results_vec.is_empty()\n                        {\n                            self.result_cursor -= 1;\n                            let off = self.results_vec[self.result_cursor];\n                            app.search_focus(off);\n                            self.scroll_to = Some(self.result_cursor);\n                        }\n                        ui.label((self.result_cursor + 1).to_string());\n                        if (ui.button(\"Next (N)\").clicked()\n                            || ui.input(|inp| inp.key_pressed(egui::Key::N)))\n                            && self.result_cursor + 1 < self.results_vec.len()\n                        {\n                            self.result_cursor += 1;\n                            let off = self.results_vec[self.result_cursor];\n                            app.search_focus(off);\n                            self.scroll_to = Some(self.result_cursor);\n                        }\n                        ui.label(format!(\"{} results\", self.results_vec.len()));\n                    });\n                });\n                strip.cell(|ui| {\n                    ui.horizontal(|ui| {\n                        if ui.button(\"Copy offsets\").clicked() {\n                            let s = self.results_vec.iter().map(ToString::to_string).join(\" \");\n                            set_clipboard_string(&mut app.clipboard, &mut gui.msg_dialog, &s);\n                        }\n                        if ui.button(\"Paste offsets\").clicked() {\n                            let s = get_clipboard_string(&mut app.clipboard, &mut gui.msg_dialog);\n                            let offsets: Result<Vec<usize>, _> =\n                                s.split_ascii_whitespace().map(|s| s.parse()).collect();\n                            match offsets {\n                                Ok(offs) => self.results_vec = offs,\n                                Err(e) => {\n                                    msg_fail(&e, \"failed to parse offsets\", &mut gui.msg_dialog);\n                                }\n                            }\n                        }\n                        if ui.button(\"🗑 Clear\").clicked() {\n                            self.results_vec.clear();\n                        }\n                        // We don't want to highlight results by default, because\n                        // it (at the very least) doubles memory usage for find results,\n                        // which can be catastrophic for really large searches.\n                        if ui.button(\"💡 Highlight\").clicked() {\n                            gui.highlight_set.clear();\n                            for &offset in &self.results_vec {\n                                gui.highlight_set.insert(offset);\n                            }\n                        }\n                    });\n                });\n            });\n    }\n\n    fn title(&self) -> &str {\n        \"Find\"\n    }\n}\n\nimpl FindDialog {\n    fn search_region(&self, app_hex_ui: &HexUi) -> Option<Region> {\n        if self.selection_only {\n            app_hex_ui.selection()\n        } else {\n            None\n        }\n    }\n    fn data_to_search<'a>(&self, app: &'a crate::app::App) -> (&'a [u8], usize) {\n        match self.search_region(&app.hex_ui) {\n            Some(reg) => (&app.data[reg.begin..=reg.end], reg.begin),\n            None => (&app.data[..], 0),\n        }\n    }\n\n    fn reload_data(&self, app: &mut crate::app::App, gui: &mut crate::gui::Gui) {\n        let result = match self.search_region(&app.hex_ui) {\n            Some(reg) => app.reload_range(reg.begin, reg.end),\n            None => app.reload(),\n        };\n        msg_if_fail(result, \"Failed to reload\", &mut gui.msg_dialog);\n    }\n}\n\ntrait SliceExt<T> {\n    fn get_array<const N: usize>(&self, offset: usize) -> Option<&[T; N]>;\n    fn get_array_mut<const N: usize>(&mut self, offset: usize) -> Option<&mut [T; N]>;\n}\n\nimpl<T> SliceExt<T> for [T] {\n    fn get_array<const N: usize>(&self, offset: usize) -> Option<&[T; N]> {\n        self.get(offset..offset + N)?.try_into().ok()\n    }\n    fn get_array_mut<const N: usize>(&mut self, offset: usize) -> Option<&mut [T; N]> {\n        self.get_mut(offset..offset + N)?.try_into().ok()\n    }\n}\n\nfn data_value_label<N: EndianedPrimitive>(\n    ui: &mut Ui,\n    data: &mut crate::data::Data,\n    off: usize,\n) -> Option<DamageRegion>\nwhere\n    [(); N::BYTE_LEN]:,\n{\n    let Some(data) = data.get_array_mut(off) else {\n        if let Some(immut) = data.get_array(off) {\n            ui.label(N::from_bytes(*immut).to_string());\n        } else {\n            ui.label(\"!!\").on_hover_text(\"Out of bounds\");\n        }\n        return None;\n    };\n    let mut n = N::from_bytes(*data);\n    if ui.add(egui::DragValue::new(&mut n)).changed() {\n        *data = N::to_bytes(n);\n        return Some(DamageRegion::Range(off..off + N::BYTE_LEN));\n    }\n    None\n}\n\nenum Action {\n    None,\n    RemoveRegionFromResults(crate::meta::RegionKey),\n    RemoveIdxFromResults(usize),\n}\n\nfn do_search(\n    data: &[u8],\n    initial_offset: usize,\n    win: &mut FindDialog,\n    gui: &mut crate::gui::Gui,\n) -> anyhow::Result<()> {\n    // Reset the result cursor, so it's not out of bounds if new results_vec is smaller\n    // TODO: Review everything to use `initial_offset` correctly\n    win.result_cursor = 0;\n    if !win.filter_results {\n        win.results_vec.clear();\n    }\n    match win.find_type {\n        FindType::I8 => find_num::<I8>(win, data)?,\n        FindType::U8 => find_u8(win, data, initial_offset, &mut gui.msg_dialog),\n        FindType::I16Le => find_num::<I16Le>(win, data)?,\n        FindType::I16Be => find_num::<I16Be>(win, data)?,\n        FindType::U16Le => find_num::<U16Le>(win, data)?,\n        FindType::U16Be => find_num::<U16Be>(win, data)?,\n        FindType::I32Le => find_num::<I32Le>(win, data)?,\n        FindType::I32Be => find_num::<I32Be>(win, data)?,\n        FindType::U32Le => find_num::<U32Le>(win, data)?,\n        FindType::U32Be => find_num::<U32Be>(win, data)?,\n        FindType::I64Le => find_num::<I64Le>(win, data)?,\n        FindType::I64Be => find_num::<I64Be>(win, data)?,\n        FindType::U64Le => find_num::<U64Le>(win, data)?,\n        FindType::U64Be => find_num::<U64Be>(win, data)?,\n        FindType::F32Le => find_num::<F32Le>(win, data)?,\n        FindType::F32Be => find_num::<F32Be>(win, data)?,\n        FindType::F64Le => find_num::<F64Le>(win, data)?,\n        FindType::F64Be => find_num::<F64Be>(win, data)?,\n        FindType::Ascii => {\n            for offset in memchr::memmem::find_iter(data, &win.find_input) {\n                win.results_vec.push(initial_offset + offset);\n            }\n        }\n        FindType::HexString => {\n            let fun = |offset| {\n                win.results_vec.push(initial_offset + offset);\n            };\n            let result = crate::find_util::find_hex_string(&win.find_input, data, fun);\n            msg_if_fail(result, \"Hex string search error\", &mut gui.msg_dialog);\n        }\n        FindType::StringDiff => {\n            let diff = ascii_to_diff_pattern(win.find_input.as_bytes());\n            let mut off = 0;\n            while let Some(offset) = find_diff_pattern(&data[off..], &diff) {\n                off += offset;\n                win.results_vec.push(initial_offset + off);\n                off += diff.len();\n            }\n        }\n        FindType::EqPattern => {\n            let needle = make_eq_pattern_needle(&win.find_input);\n            let mut off = 0;\n            while let Some(offset) = find_eq_pattern_needle(&needle, &data[off..]) {\n                off += offset;\n                win.results_vec.push(initial_offset + off);\n                off += needle.len();\n            }\n        }\n    }\n    Ok(())\n}\n\nfn make_eq_pattern_needle(pattern: &str) -> Vec<u8> {\n    let mut needle = Vec::new();\n    let mut map = HashMap::new();\n    let mut uniq_counter = 0u8;\n    for b in pattern.bytes() {\n        let val = map.entry(b).or_insert_with(|| {\n            let val = uniq_counter;\n            uniq_counter += 1;\n            val\n        });\n        needle.push(*val);\n    }\n    needle\n}\n\n#[test]\nfn test_make_eq_pattern_needle() {\n    assert_eq!(\n        make_eq_pattern_needle(\"ABCDBEFFG\"),\n        &[0, 1, 2, 3, 1, 4, 5, 5, 6]\n    );\n    assert_eq!(\n        make_eq_pattern_needle(\"abcdefggheijkbbl\"),\n        &[0, 1, 2, 3, 4, 5, 6, 6, 7, 4, 8, 9, 10, 1, 1, 11]\n    );\n}\n\n#[cfg(test)]\nfn find_eq_pattern(pattern: &str, data: &[u8]) -> Option<usize> {\n    let needle = make_eq_pattern_needle(pattern);\n    find_eq_pattern_needle(&needle, data)\n}\n\nfn find_eq_pattern_needle(needle: &[u8], data: &[u8]) -> Option<usize> {\n    for window in data.windows(needle.len()) {\n        if eq_pattern_needle_matches(needle, window) {\n            return Some(window.as_ptr() as usize - data.as_ptr() as usize);\n        }\n    }\n    None\n}\n\nfn eq_pattern_needle_matches(needle: &[u8], data: &[u8]) -> bool {\n    for (n1, d1) in needle.iter().zip(data.iter()) {\n        for (n2, d2) in needle.iter().zip(data.iter()) {\n            if (n1 == n2) != (d1 == d2) {\n                return false;\n            }\n        }\n    }\n    true\n}\n\n#[test]\nfn test_find_eq_pattern() {\n    assert_eq!(find_eq_pattern(\"ABCDBEFFG\", b\"I AM GOOD\"), Some(0));\n    assert_eq!(\n        find_eq_pattern(\"abcdefggheijkbbk\", b\"Hello world, very cool indeed\"),\n        Some(13)\n    );\n}\n\n#[expect(clippy::cast_possible_wrap)]\nfn ascii_to_diff_pattern(ascii: &[u8]) -> Vec<i8> {\n    ascii.array_windows().map(|[a, b]| *b as i8 - *a as i8).collect()\n}\n\n#[expect(clippy::cast_possible_wrap)]\nfn find_diff_pattern(haystack: &[u8], pat: &[i8]) -> Option<usize> {\n    assert!(pat.len() <= haystack.len());\n    let mut pat_cur = 0;\n    for (i, [a, b]) in haystack.array_windows().enumerate() {\n        let Some(diff) = (*b as i8).checked_sub(*a as i8) else {\n            pat_cur = 0;\n            continue;\n        };\n        if diff == pat[pat_cur] {\n            pat_cur += 1;\n        } else {\n            pat_cur = 0;\n        }\n        if pat_cur == pat.len() {\n            return Some((i + 1) - pat.len());\n        }\n    }\n    None\n}\n\n#[test]\nfn test_ascii_to_diff_pattern() {\n    assert_eq!(\n        ascii_to_diff_pattern(b\"jonathan\"),\n        vec![5, -1, -13, 19, -12, -7, 13]\n    );\n}\n\n#[test]\n#[expect(clippy::unwrap_used)]\nfn test_find_diff_pattern() {\n    let key = \"jonathan\";\n    let pat = ascii_to_diff_pattern(key.as_bytes());\n    let s = \"I handed the key to jonathan. He didn't like the way I said jonathan to him.\";\n    let mut off = 0;\n    off += find_diff_pattern(&s.as_bytes()[off..], &pat).unwrap();\n    assert_eq!(&s[off..off + key.len()], key);\n    off += pat.len();\n    off += find_diff_pattern(&s.as_bytes()[off..], &pat).unwrap();\n    assert_eq!(&s[off..off + key.len()], key);\n}\n\nfn find_num<N: EndianedPrimitive>(win: &mut FindDialog, data: &[u8]) -> Result<(), anyhow::Error>\nwhere\n    [(); N::BYTE_LEN]:,\n    <<N as EndianedPrimitive>::Primitive as FromStr>::Err: Error + Send + Sync,\n{\n    find_num_raw::<N>(&win.find_input, data, |offset| {\n        win.results_vec.push(offset);\n    })\n}\n\npub(crate) fn find_num_raw<N: EndianedPrimitive>(\n    input: &str,\n    data: &[u8],\n    mut f: impl FnMut(usize),\n) -> anyhow::Result<()>\nwhere\n    [(); N::BYTE_LEN]:,\n    <<N as EndianedPrimitive>::Primitive as FromStr>::Err: Error + Send + Sync,\n{\n    let n: N::Primitive = input.parse()?;\n    let bytes = N::to_bytes(n);\n    for offset in memchr::memmem::find_iter(data, &bytes) {\n        f(offset);\n    }\n    Ok(())\n}\n\nfn find_u8(dia: &mut FindDialog, data: &[u8], initial_offset: usize, msg: &mut MessageDialog) {\n    // TODO: This is probably a minefield for initial_offset shenanigans.\n    // Need to review carefully\n    match dia.find_input.as_str() {\n        \"?\" => {\n            dia.data_snapshot = data.to_vec();\n            dia.results_vec.clear();\n            for i in 0..data.len() {\n                dia.results_vec.push(initial_offset + i);\n            }\n        }\n        \">\" => {\n            if dia.filter_results {\n                dia.results_vec.retain(|&offset| {\n                    data[offset - initial_offset] > dia.data_snapshot[offset - initial_offset]\n                });\n            } else {\n                for (i, (&new, &old)) in data.iter().zip(dia.data_snapshot.iter()).enumerate() {\n                    if new > old {\n                        dia.results_vec.push(i);\n                    }\n                }\n            }\n            dia.data_snapshot = data.to_vec();\n        }\n        \"=\" => {\n            eq_filter(dia, data, initial_offset);\n        }\n        \"!=\" => {\n            if dia.filter_results {\n                dia.results_vec.retain(|&offset| {\n                    data[offset - initial_offset] != dia.data_snapshot[offset - initial_offset]\n                });\n            } else {\n                for (i, (&new, &old)) in data.iter().zip(dia.data_snapshot.iter()).enumerate() {\n                    if new == old {\n                        dia.results_vec.push(i);\n                    }\n                }\n            }\n            dia.data_snapshot = data.to_vec();\n        }\n        \"<\" => {\n            if dia.filter_results {\n                dia.results_vec.retain(|&offset| {\n                    data[offset - initial_offset] < dia.data_snapshot[offset - initial_offset]\n                });\n            } else {\n                for (i, (&new, &old)) in data.iter().zip(dia.data_snapshot.iter()).enumerate() {\n                    if new < old {\n                        dia.results_vec.push(i);\n                    }\n                }\n            }\n            dia.data_snapshot = data.to_vec();\n        }\n        _ => match parse_guess_radix(&dia.find_input) {\n            Ok(needle) => {\n                if dia.filter_results {\n                    let results_vec_clone = dia.results_vec.clone();\n                    dia.results_vec.clear();\n                    u8_search(\n                        dia,\n                        results_vec_clone.iter().map(|&off| (off, data[off])),\n                        initial_offset,\n                        needle,\n                    );\n                } else {\n                    u8_search(\n                        dia,\n                        data.iter().cloned().enumerate(),\n                        initial_offset,\n                        needle,\n                    );\n                }\n            }\n            Err(e) => msg.open(Icon::Error, \"Parse error\", e.to_string()),\n        },\n    }\n}\n\nfn eq_filter(dia: &mut FindDialog, data: &[u8], initial_offset: usize) {\n    if dia.filter_results {\n        dia.results_vec.retain(|&offset| {\n            data[offset - initial_offset] == dia.data_snapshot[offset - initial_offset]\n        });\n    } else {\n        for (i, (&new, &old)) in data.iter().zip(dia.data_snapshot.iter()).enumerate() {\n            if new == old {\n                dia.results_vec.push(i);\n            }\n        }\n    }\n    dia.data_snapshot = data.to_vec();\n}\n\nfn u8_search(\n    dialog: &mut FindDialog,\n    haystack: impl Iterator<Item = (usize, u8)>,\n    initial_offset: usize,\n    needle: u8,\n) {\n    for (offset, byte) in haystack {\n        if byte == needle {\n            dialog.results_vec.push(initial_offset + offset);\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/find_memory_pointers.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::shell::msg_fail,\n    egui_extras::{Column, TableBuilder},\n};\n\n#[derive(Default)]\npub struct FindMemoryPointersWindow {\n    pub open: WindowOpen,\n    pointers: Vec<PtrEntry>,\n    filter_write: bool,\n    filter_exec: bool,\n}\n\n#[derive(Clone, Copy)]\nstruct PtrEntry {\n    src_idx: usize,\n    ptr: usize,\n    range_idx: usize,\n    write: bool,\n    execute: bool,\n}\n\nimpl super::Window for FindMemoryPointersWindow {\n    fn ui(\n        &mut self,\n        WinCtx {\n            ui,\n            gui,\n            app,\n            font_size,\n            line_spacing,\n            ..\n        }: WinCtx,\n    ) {\n        ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n        let Some(pid) = gui.win.open_process.selected_pid else {\n            ui.label(\"No selected pid.\");\n            return;\n        };\n        if self.open.just_now() {\n            for (i, wnd) in app.data.array_windows::<{ (usize::BITS / 8) as usize }>().enumerate() {\n                let ptr = usize::from_le_bytes(*wnd);\n                if let Some(pos) = gui.win.open_process.map_ranges.iter().position(|range| {\n                    range.is_read() && range.start() <= ptr && range.start() + range.size() >= ptr\n                }) {\n                    let range = &gui.win.open_process.map_ranges[pos];\n                    self.pointers.push(PtrEntry {\n                        src_idx: i,\n                        ptr,\n                        range_idx: pos,\n                        write: range.is_write(),\n                        execute: range.is_exec(),\n                    });\n                }\n            }\n        }\n        let mut action = Action::None;\n        TableBuilder::new(ui)\n            .column(Column::auto())\n            .column(Column::auto())\n            .column(Column::auto())\n            .column(Column::remainder())\n            .striped(true)\n            .resizable(true)\n            .header(20.0, |mut row| {\n                row.col(|ui| {\n                    ui.label(\"Location\");\n                });\n                row.col(|ui| {\n                    if ui.button(\"Region\").clicked() {\n                        self.pointers.sort_by_key(|p| {\n                            gui.win.open_process.map_ranges[p.range_idx].filename()\n                        });\n                    }\n                });\n                row.col(|ui| {\n                    ui.menu_button(\"w/x\", |ui| {\n                        ui.checkbox(&mut self.filter_write, \"Write\");\n                        ui.checkbox(&mut self.filter_exec, \"Execute\");\n                    });\n                });\n                row.col(|ui| {\n                    if ui.button(\"Pointer\").clicked() {\n                        self.pointers.sort_by_key(|p| p.ptr);\n                    }\n                });\n            })\n            .body(|body| {\n                let mut filtered = self.pointers.clone();\n                filtered.retain(|ptr| {\n                    if self.filter_exec && !ptr.execute {\n                        return false;\n                    }\n                    if self.filter_write && !ptr.write {\n                        return false;\n                    }\n                    true\n                });\n                body.rows(20.0, filtered.len(), |mut row| {\n                    let en = &filtered[row.index()];\n                    row.col(|ui| {\n                        if ui.link(format!(\"{:X}\", en.src_idx)).clicked() {\n                            action = Action::Goto(en.src_idx);\n                        }\n                    });\n                    row.col(|ui| {\n                        let range = &gui.win.open_process.map_ranges[en.range_idx];\n                        ui.label(range.filename().map_or_else(\n                            || format!(\"<anon> @ {:X} (size: {})\", range.start(), range.size()),\n                            |p| p.display().to_string(),\n                        ));\n                    });\n                    row.col(|ui| {\n                        let range = &gui.win.open_process.map_ranges[en.range_idx];\n                        ui.label(format!(\n                            \"{}{}\",\n                            if range.is_write() { \"w\" } else { \"\" },\n                            if range.is_exec() { \"x\" } else { \"\" }\n                        ));\n                    });\n                    row.col(|ui| {\n                        let range = &gui.win.open_process.map_ranges[en.range_idx];\n                        if ui.link(format!(\"{:X}\", en.ptr)).clicked() {\n                            match app.load_proc_memory(\n                                pid,\n                                range.start(),\n                                range.size(),\n                                range.is_write(),\n                                &mut gui.msg_dialog,\n                                font_size,\n                                line_spacing,\n                            ) {\n                                Ok(()) => action = Action::Goto(en.ptr - range.start()),\n                                Err(e) => {\n                                    msg_fail(&e, \"failed to load proc memory\", &mut gui.msg_dialog);\n                                }\n                            }\n                        }\n                    });\n                });\n            });\n        match action {\n            Action::Goto(off) => {\n                app.center_view_on_offset(off);\n                app.edit_state.set_cursor(off);\n                app.hex_ui.flash_cursor();\n            }\n            Action::None => {}\n        }\n    }\n\n    fn title(&self) -> &str {\n        \"Find memory pointers\"\n    }\n}\n\nenum Action {\n    Goto(usize),\n    None,\n}\n"
  },
  {
    "path": "src/gui/windows/layouts.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        app::App,\n        meta::{LayoutKey, LayoutMapExt as _, MetaLow, NamedView, ViewKey, ViewMap},\n        view::{HexData, TextData, View, ViewKind},\n    },\n    constcat::concat,\n    egui_phosphor::regular as ic,\n    egui_sf2g::sf2g::graphics::Font,\n    slotmap::Key as _,\n};\n\nconst L_NEW_FROM_PERSPECTIVE: &str = concat!(ic::PLUS, \" New from perspective\");\nconst L_HEX: &str = concat!(ic::HEXAGON, \" Hex\");\nconst L_TEXT: &str = concat!(ic::TEXT_AA, \" Text\");\nconst L_BLOCK: &str = concat!(ic::RECTANGLE, \" Block\");\nconst L_ADD_TO_NEW_ROW: &str = concat!(ic::PLUS, ic::ARROW_BEND_DOWN_RIGHT);\nconst L_ADD_TO_CURRENT_ROW: &str = concat!(ic::PLUS, ic::ARROW_LEFT);\n\n#[derive(Default)]\npub struct LayoutsWindow {\n    pub open: WindowOpen,\n    selected: LayoutKey,\n    swap_a: ViewKey,\n    edit_name: bool,\n}\n\nimpl super::Window for LayoutsWindow {\n    fn ui(\n        &mut self,\n        WinCtx {\n            ui,\n            gui,\n            app,\n            font_size,\n            font,\n            ..\n        }: WinCtx,\n    ) {\n        if self.open.just_now() {\n            self.selected = app.hex_ui.current_layout;\n        }\n        for (k, v) in &app.meta_state.meta.layouts {\n            if ui.selectable_label(self.selected == k, &v.name).clicked() {\n                self.selected = k;\n                App::switch_layout(&mut app.hex_ui, &app.meta_state.meta, k);\n            }\n        }\n        if !self.selected.is_null() {\n            ui.separator();\n            let Some(layout) = app.meta_state.meta.layouts.get_mut(self.selected) else {\n                self.selected = LayoutKey::null();\n                return;\n            };\n            ui.horizontal(|ui| {\n                if self.edit_name {\n                    if ui.text_edit_singleline(&mut layout.name).lost_focus() {\n                        self.edit_name = false;\n                    }\n                } else {\n                    ui.heading(&layout.name);\n                }\n                if ui.button(\"✏\").clicked() {\n                    self.edit_name ^= true;\n                }\n            });\n            let unused_views: Vec<ViewKey> = app\n                .meta_state\n                .meta\n                .views\n                .keys()\n                .filter(|&k| !layout.iter().any(|k2| k2 == k))\n                .collect();\n            egui::Grid::new(\"view_grid\").show(ui, |ui| {\n                let mut swap = None;\n                layout.view_grid.retain_mut(|row| {\n                    let mut retain_row = true;\n                    row.retain_mut(|view_key| {\n                        let mut retain = true;\n                        let view = &app.meta_state.meta.views[*view_key];\n                        if self.swap_a == *view_key {\n                            if ui.selectable_label(true, &view.name).clicked() {\n                                self.swap_a = ViewKey::null();\n                            }\n                        } else if !self.swap_a.is_null() {\n                            if ui.button(format!(\"🔃 {}\", view.name)).clicked() {\n                                swap = Some((self.swap_a, *view_key));\n                            }\n                        } else {\n                            ui.menu_button([ic::EYE, \" \", view.name.as_str()].concat(), |ui| {\n                                for &k in &unused_views {\n                                    if ui.button(&app.meta_state.meta.views[k].name).clicked() {\n                                        *view_key = k;\n                                    }\n                                }\n                                if unused_views.is_empty() {\n                                    ui.label(egui::RichText::new(\"No unused views\").italics());\n                                }\n                            })\n                            .response\n                            .context_menu(|ui| {\n                                if ui.button(\"🔃 Swap\").clicked() {\n                                    self.swap_a = *view_key;\n                                }\n                                if ui.button(\"🗑 Remove\").clicked() {\n                                    retain = false;\n                                }\n                                if ui.button(\"👁 View properties\").clicked() {\n                                    gui.win.views.open.set(true);\n                                    gui.win.views.selected = *view_key;\n                                }\n                            });\n                        }\n\n                        retain\n                    });\n                    ui.menu_button(L_ADD_TO_CURRENT_ROW, |ui| {\n                        for &k in &unused_views {\n                            if ui\n                                .button(\n                                    [ic::EYE, \" \", app.meta_state.meta.views[k].name.as_str()]\n                                        .concat(),\n                                )\n                                .clicked()\n                            {\n                                row.push(k);\n                            }\n                        }\n                        if let Some(k) = add_new_view_menu(\n                            ui,\n                            &app.meta_state.meta.low,\n                            &mut app.meta_state.meta.views,\n                            font_size,\n                            font,\n                        ) {\n                            row.push(k);\n                        }\n                    })\n                    .response\n                    .on_hover_text(\"Add to current row\");\n                    if ui.button(\"🗑\").on_hover_text(\"Delete row\").clicked() {\n                        retain_row = false;\n                    }\n                    ui.end_row();\n                    if row.is_empty() {\n                        retain_row = false;\n                    }\n                    retain_row\n                });\n                if let Some((a, b)) = swap\n                    && let Some([a_row, a_col]) = layout.idx_of_key(a)\n                    && let Some([b_row, b_col]) = layout.idx_of_key(b)\n                {\n                    let addr_a = &raw mut layout.view_grid[a_row][a_col];\n                    let addr_b = &raw mut layout.view_grid[b_row][b_col];\n                    // Safety: `addr_a` and `addr_b` are r/w valid and well-aligned\n                    unsafe {\n                        std::ptr::swap(addr_a, addr_b);\n                    }\n                    self.swap_a = ViewKey::null();\n                }\n                ui.menu_button(L_ADD_TO_NEW_ROW, |ui| {\n                    for &k in &unused_views {\n                        if ui\n                            .button(\n                                [ic::EYE, \" \", app.meta_state.meta.views[k].name.as_str()].concat(),\n                            )\n                            .clicked()\n                        {\n                            layout.view_grid.push(vec![k]);\n                        }\n                    }\n                    if let Some(k) = add_new_view_menu(\n                        ui,\n                        &app.meta_state.meta.low,\n                        &mut app.meta_state.meta.views,\n                        font_size,\n                        font,\n                    ) {\n                        layout.view_grid.push(vec![k]);\n                        app.hex_ui.focused_view = Some(k);\n                    }\n                })\n                .response\n                .on_hover_text(\"Add to new row\")\n            });\n            ui.horizontal(|ui| {\n                ui.label(\"Margin\");\n                ui.label(\"x\");\n                ui.add(egui::DragValue::new(&mut layout.margin.x).range(3..=64));\n                ui.label(\"y\");\n                ui.add(egui::DragValue::new(&mut layout.margin.y).range(3..=64));\n            });\n        }\n        ui.separator();\n        if ui.button(\"New layout\").clicked() {\n            let key = app.meta_state.meta.layouts.add_new_default();\n            self.selected = key;\n            App::switch_layout(&mut app.hex_ui, &app.meta_state.meta, key);\n        }\n    }\n\n    fn title(&self) -> &str {\n        \"Layouts\"\n    }\n}\n\nfn add_new_view_menu(\n    ui: &mut egui::Ui,\n    low: &MetaLow,\n    views: &mut ViewMap,\n    font_size: u16,\n    font: &Font,\n) -> Option<ViewKey> {\n    let mut ret_key = None;\n    ui.separator();\n    ui.menu_button(L_NEW_FROM_PERSPECTIVE, |ui| {\n        for (per_key, per) in &low.perspectives {\n            ui.menu_button([ic::PERSPECTIVE, \" \", per.name.as_str()].concat(), |ui| {\n                let mut new = None;\n                if ui.button(L_HEX).clicked() {\n                    let view =\n                        View::new(ViewKind::Hex(HexData::with_font_size(font_size)), per_key);\n                    new = Some((\"hex\", view));\n                }\n                if ui.button(L_TEXT).clicked() {\n                    let view = View::new(\n                        #[expect(clippy::cast_sign_loss, clippy::cast_possible_truncation)]\n                        ViewKind::Text(TextData::with_font_info(\n                            font.line_spacing(font_size.into()) as _,\n                            font_size,\n                        )),\n                        per_key,\n                    );\n                    new = Some((\"text\", view));\n                }\n                if ui.button(L_BLOCK).clicked() {\n                    let view = View::new(ViewKind::Block, per_key);\n                    new = Some((\"block\", view));\n                }\n                if let Some((label, view)) = new {\n                    let view_key = views.insert(NamedView {\n                        view,\n                        name: [per.name.as_str(), \" \", label].concat(),\n                    });\n                    ret_key = Some(view_key);\n                }\n            });\n        }\n    });\n    ret_key\n}\n"
  },
  {
    "path": "src/gui/windows/lua_console.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{meta::ScriptKey, scripting::exec_lua, shell::msg_if_fail},\n    std::{collections::HashMap, fmt::Write as _},\n};\n\ntype MsgBuf = Vec<ConMsg>;\ntype MsgBufMap = HashMap<ScriptKey, MsgBuf>;\n\n#[derive(Default)]\npub struct LuaConsoleWindow {\n    pub open: WindowOpen,\n    pub msg_bufs: MsgBufMap,\n    pub eval_buf: String,\n    pub active_msg_buf: Option<ScriptKey>,\n    pub default_msg_buf: MsgBuf,\n}\n\nimpl LuaConsoleWindow {\n    fn msg_buf(&mut self) -> &mut MsgBuf {\n        match self.active_msg_buf {\n            Some(key) => self.msg_bufs.get_mut(&key).unwrap_or(&mut self.default_msg_buf),\n            None => &mut self.default_msg_buf,\n        }\n    }\n    pub fn msg_buf_for_key(&mut self, key: Option<ScriptKey>) -> &mut MsgBuf {\n        match key {\n            Some(key) => self.msg_bufs.entry(key).or_default(),\n            None => &mut self.default_msg_buf,\n        }\n    }\n}\n\npub enum ConMsg {\n    Plain(String),\n    OffsetLink {\n        text: String,\n        offset: usize,\n    },\n    RangeLink {\n        text: String,\n        start: usize,\n        end: usize,\n    },\n}\n\nimpl super::Window for LuaConsoleWindow {\n    fn ui(\n        &mut self,\n        WinCtx {\n            ui,\n            gui,\n            app,\n            lua,\n            font_size,\n            line_spacing,\n            ..\n        }: WinCtx,\n    ) {\n        ui.horizontal(|ui| {\n            if ui.selectable_label(self.active_msg_buf.is_none(), \"Default\").clicked() {\n                self.active_msg_buf = None;\n            }\n            for k in self.msg_bufs.keys() {\n                if ui\n                    .selectable_label(\n                        self.active_msg_buf == Some(*k),\n                        &app.meta_state.meta.scripts[*k].name,\n                    )\n                    .clicked()\n                {\n                    self.active_msg_buf = Some(*k);\n                }\n            }\n        });\n        ui.separator();\n        ui.horizontal(|ui| {\n            let re = ui.text_edit_singleline(&mut self.eval_buf);\n            if ui.button(\"x\").on_hover_text(\"Clear input\").clicked() {\n                self.eval_buf.clear();\n            }\n            if ui.button(\"Eval\").clicked()\n                || (ui.input(|inp| inp.key_pressed(egui::Key::Enter)) && re.lost_focus())\n            {\n                let code = &self.eval_buf.clone();\n                if let Err(e) = exec_lua(\n                    lua,\n                    code,\n                    app,\n                    gui,\n                    \"\",\n                    self.active_msg_buf,\n                    font_size,\n                    line_spacing,\n                ) {\n                    self.msg_buf().push(ConMsg::Plain(e.to_string()));\n                }\n            }\n            if ui.button(\"Clear log\").clicked() {\n                self.msg_buf().clear();\n            }\n            if ui.button(\"Copy to clipboard\").clicked() {\n                let mut buf = String::new();\n                for msg in self.msg_buf() {\n                    match msg {\n                        ConMsg::Plain(s) => {\n                            buf.push_str(s);\n                            buf.push('\\n');\n                        }\n                        ConMsg::OffsetLink { text, offset } => {\n                            let _ = writeln!(&mut buf, \"{offset}: {text}\");\n                        }\n                        ConMsg::RangeLink { text, start, end } => {\n                            let _ = writeln!(&mut buf, \"{start}..={end}: {text}\");\n                        }\n                    }\n                }\n                msg_if_fail(\n                    app.clipboard.set_text(buf),\n                    \"Failed to copy clipboard text\",\n                    &mut gui.msg_dialog,\n                );\n            }\n        });\n        ui.separator();\n        egui::ScrollArea::vertical().auto_shrink([false, true]).show(ui, |ui| {\n            for msg in &*self.msg_buf() {\n                match msg {\n                    ConMsg::Plain(text) => {\n                        ui.label(text);\n                    }\n                    ConMsg::OffsetLink { text, offset } => {\n                        if ui.link(text).clicked() {\n                            app.search_focus(*offset);\n                        }\n                    }\n                    ConMsg::RangeLink { text, start, end } => {\n                        if ui.link(text).clicked() {\n                            app.hex_ui.select_a = Some(*start);\n                            app.hex_ui.select_b = Some(*end);\n                            app.search_focus(*start);\n                        }\n                    }\n                }\n            }\n        });\n    }\n\n    fn title(&self) -> &str {\n        \"Lua quick eval\"\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/lua_editor.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        app::App,\n        gui::Gui,\n        meta::{Script, ScriptKey},\n        scripting::SCRIPT_ARG_FMT_HELP_STR,\n        shell::msg_if_fail,\n        str_ext::StrExt as _,\n    },\n    egui::TextBuffer as _,\n    egui_code_editor::{CodeEditor, Syntax},\n    egui_extras::{Size, StripBuilder},\n    mlua::Lua,\n    std::time::Instant,\n};\n\n#[derive(Default)]\npub struct LuaEditorWindow {\n    pub open: WindowOpen,\n    result_info_string: String,\n    err: bool,\n    new_script_name: String,\n    args_string: String,\n    edit_key: Option<ScriptKey>,\n}\n\nimpl super::Window for LuaEditorWindow {\n    fn ui(&mut self, ctx: WinCtx) {\n        let WinCtx {\n            ui,\n            gui,\n            app,\n            lua,\n            font_size,\n            line_spacing,\n            ..\n        } = ctx;\n        let ctrl_enter =\n            ui.input_mut(|inp| inp.consume_key(egui::Modifiers::CTRL, egui::Key::Enter));\n        let ctrl_s = ui.input_mut(|inp| inp.consume_key(egui::Modifiers::CTRL, egui::Key::S));\n        if ctrl_s {\n            msg_if_fail(\n                app.save(&mut gui.msg_dialog),\n                \"Failed to save\",\n                &mut gui.msg_dialog,\n            );\n        }\n        StripBuilder::new(ui).size(Size::remainder()).size(Size::exact(300.0)).vertical(\n            |mut strip| {\n                strip.cell(|ui| {\n                    egui::ScrollArea::vertical().show(ui, |ui| {\n                        let lua;\n                        match self.edit_key {\n                            Some(key) => match app.meta_state.meta.scripts.get_mut(key) {\n                                Some(script) => lua = &mut script.content,\n                                None => {\n                                    eprintln!(\n                                        \"Edit key is no longer in meta state. Setting to None.\"\n                                    );\n                                    self.edit_key = None;\n                                    return;\n                                }\n                            },\n                            None => lua = &mut app.meta_state.meta.misc.exec_lua_script,\n                        }\n                        CodeEditor::default().with_syntax(Syntax::lua()).show(ui, lua);\n                    });\n                });\n                strip.cell(|ui| {\n                    ui.separator();\n                    ui.horizontal(|ui| {\n                        if ui.button(\"⚡ Execute\").on_hover_text(\"Ctrl+Enter\").clicked()\n                            || ctrl_enter\n                        {\n                            self.exec_lua(app, lua, gui, font_size, line_spacing);\n                        }\n                        let script_label = match &self.edit_key {\n                            Some(key) => {\n                                let scr = &app.meta_state.meta.scripts[*key];\n                                &scr.name\n                            }\n                            None => \"<Unnamed>\",\n                        };\n                        egui::ComboBox::from_label(\"Script\").selected_text(script_label).show_ui(\n                            ui,\n                            |ui| {\n                                if ui\n                                    .selectable_label(self.edit_key.is_none(), \"<Unnamed>\")\n                                    .clicked()\n                                {\n                                    self.edit_key = None;\n                                }\n                                ui.separator();\n                                for (k, v) in app.meta_state.meta.scripts.iter() {\n                                    if ui\n                                        .selectable_label(self.edit_key == Some(k), &v.name)\n                                        .clicked()\n                                    {\n                                        self.edit_key = Some(k);\n                                    }\n                                }\n                            },\n                        );\n                        if ui.button(\"🖴 Load from file...\").clicked() {\n                            gui.fileops.load_lua_script();\n                        }\n                        if ui.button(\"💾 Save to file...\").clicked() {\n                            gui.fileops.save_lua_script();\n                        }\n                        if ui.button(\"？ Help\").clicked() {\n                            gui.win.lua_help.open.toggle();\n                        }\n                    });\n                    ui.horizontal(|ui| {\n                        ui.add(\n                            egui::TextEdit::singleline(&mut self.new_script_name)\n                                .hint_text(\"New script name\"),\n                        );\n                        if ui\n                            .add_enabled(\n                                !self.new_script_name.is_empty_or_ws_only(),\n                                egui::Button::new(\"Add named script\"),\n                            )\n                            .clicked()\n                        {\n                            let key = app.meta_state.meta.scripts.insert(Script {\n                                name: self.new_script_name.take(),\n                                desc: String::new(),\n                                content: app.meta_state.meta.misc.exec_lua_script.clone(),\n                            });\n                            self.edit_key = Some(key);\n                        }\n                    });\n                    ui.horizontal(|ui| {\n                        ui.label(format!(\"Args ({SCRIPT_ARG_FMT_HELP_STR})\"));\n                        ui.text_edit_singleline(&mut self.args_string);\n                    });\n                    ui.separator();\n                    if app.data.dirty_region.is_some() {\n                        ui.label(\n                            egui::RichText::new(\"Unsaved changes\")\n                                .italics()\n                                .color(egui::Color32::YELLOW)\n                                .code(),\n                        );\n                    } else {\n                        ui.label(\n                            egui::RichText::new(\"No unsaved changes\")\n                                .color(egui::Color32::GREEN)\n                                .code(),\n                        );\n                    }\n                    if !self.result_info_string.is_empty() {\n                        if self.err {\n                            ui.label(\n                                egui::RichText::new(&self.result_info_string)\n                                    .color(egui::Color32::RED),\n                            );\n                        } else {\n                            ui.label(&self.result_info_string);\n                        }\n                    }\n                });\n            },\n        );\n    }\n\n    fn title(&self) -> &str {\n        \"Lua Editor\"\n    }\n}\n\nimpl LuaEditorWindow {\n    fn exec_lua(\n        &mut self,\n        app: &mut App,\n        lua: &Lua,\n        gui: &mut Gui,\n        font_size: u16,\n        line_spacing: u16,\n    ) {\n        let start_time = Instant::now();\n        let lua_script = self\n            .edit_key\n            .map_or(&app.meta_state.meta.misc.exec_lua_script, |key| {\n                &app.meta_state.meta.scripts[key].content\n            })\n            .clone();\n        let result = crate::scripting::exec_lua(\n            lua,\n            &lua_script,\n            app,\n            gui,\n            &self.args_string,\n            self.edit_key,\n            font_size,\n            line_spacing,\n        );\n        if let Err(e) = result {\n            self.result_info_string = e.to_string();\n            self.err = true;\n        } else {\n            self.result_info_string =\n                format!(\"Script took {} ms\", start_time.elapsed().as_millis());\n            self.err = false;\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/lua_help.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::scripting::*,\n    egui::Color32,\n};\n\n#[derive(Default)]\npub struct LuaHelpWindow {\n    pub open: WindowOpen,\n    pub filter: String,\n}\n\nimpl super::Window for LuaHelpWindow {\n    fn ui(&mut self, WinCtx { ui, .. }: WinCtx) {\n        ui.add(egui::TextEdit::singleline(&mut self.filter).hint_text(\"🔍 Filter\"));\n        egui::ScrollArea::vertical().max_height(500.0).show(ui, |ui| {\n            macro_rules! add_help {\n                ($t:ty) => {\n                    'block: {\n                        let filter_lower = &self.filter.to_ascii_lowercase();\n                        if !(<$t>::NAME.to_ascii_lowercase().contains(filter_lower)\n                            || <$t>::HELP.to_ascii_lowercase().contains(filter_lower))\n                        {\n                            break 'block;\n                        }\n                        ui.horizontal(|ui| {\n                            ui.style_mut().spacing.item_spacing = egui::vec2(0., 0.);\n                            ui.label(\"hx:\");\n                            ui.label(\n                                egui::RichText::new(<$t>::API_SIG).color(Color32::WHITE).strong(),\n                            );\n                        });\n                        ui.indent(\"doc_indent\", |ui| {\n                            ui.label(<$t>::HELP);\n                        });\n                    }\n                };\n            }\n            for_each_method!(add_help);\n        });\n    }\n\n    fn title(&self) -> &str {\n        \"Lua help\"\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/lua_watch.rs",
    "content": "use {super::WinCtx, crate::scripting::exec_lua};\n\npub struct LuaWatchWindow {\n    pub name: String,\n    expr: String,\n    watch: bool,\n}\n\nimpl Default for LuaWatchWindow {\n    fn default() -> Self {\n        Self {\n            name: \"New watch window\".into(),\n            expr: String::new(),\n            watch: false,\n        }\n    }\n}\n\nimpl super::Window for LuaWatchWindow {\n    fn ui(\n        &mut self,\n        WinCtx {\n            ui,\n            gui,\n            app,\n            lua,\n            font_size,\n            line_spacing,\n            ..\n        }: WinCtx,\n    ) {\n        ui.text_edit_singleline(&mut self.name);\n        ui.text_edit_singleline(&mut self.expr);\n        ui.checkbox(&mut self.watch, \"watch\");\n        if self.watch {\n            match exec_lua(lua, &self.expr, app, gui, \"\", None, font_size, line_spacing) {\n                Ok(ret) => {\n                    if let Some(s) = ret {\n                        ui.label(s);\n                    } else {\n                        ui.label(\"No output\");\n                    }\n                }\n                Err(e) => {\n                    ui.label(e.to_string());\n                }\n            }\n        }\n    }\n\n    fn title(&self) -> &str {\n        &self.name\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/meta_diff.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        layout::Layout,\n        meta::{\n            LayoutKey, NamedRegion, NamedView, PerspectiveKey, RegionKey, ViewKey,\n            perspective::Perspective,\n        },\n    },\n    itertools::{EitherOrBoth, Itertools as _},\n    slotmap::SlotMap,\n    std::fmt::Debug,\n};\n\n#[derive(Default)]\npub struct MetaDiffWindow {\n    pub open: WindowOpen,\n}\nimpl super::Window for MetaDiffWindow {\n    fn ui(&mut self, WinCtx { ui, app, .. }: WinCtx) {\n        let this = &mut app.meta_state.meta;\n        let clean = &app.meta_state.clean_meta;\n        ui.heading(\"Regions\");\n        diff_slotmap(ui, &mut this.low.regions, &clean.low.regions);\n        ui.heading(\"Perspectives\");\n        diff_slotmap(ui, &mut this.low.perspectives, &clean.low.perspectives);\n        ui.heading(\"Views\");\n        diff_slotmap(ui, &mut this.views, &clean.views);\n        ui.heading(\"Layouts\");\n        diff_slotmap(ui, &mut this.layouts, &clean.layouts);\n    }\n\n    fn title(&self) -> &str {\n        \"Diff against clean meta\"\n    }\n}\n\ntrait SlotmapDiffItem: PartialEq + Eq + Clone + Debug {\n    type Key: slotmap::Key;\n    type SortKey: Ord;\n    fn label(&self) -> &str;\n    fn sort_key(&self) -> Self::SortKey;\n}\n\nimpl SlotmapDiffItem for NamedRegion {\n    type Key = RegionKey;\n\n    fn label(&self) -> &str {\n        &self.name\n    }\n\n    type SortKey = usize;\n\n    fn sort_key(&self) -> Self::SortKey {\n        self.region.begin\n    }\n}\n\nimpl SlotmapDiffItem for Perspective {\n    type Key = PerspectiveKey;\n\n    type SortKey = String;\n\n    fn label(&self) -> &str {\n        &self.name\n    }\n\n    fn sort_key(&self) -> Self::SortKey {\n        self.name.clone()\n    }\n}\n\nimpl SlotmapDiffItem for NamedView {\n    type Key = ViewKey;\n\n    type SortKey = String;\n\n    fn label(&self) -> &str {\n        &self.name\n    }\n\n    fn sort_key(&self) -> Self::SortKey {\n        self.name.clone()\n    }\n}\n\nimpl SlotmapDiffItem for Layout {\n    type Key = LayoutKey;\n\n    type SortKey = String;\n\n    fn label(&self) -> &str {\n        &self.name\n    }\n\n    fn sort_key(&self) -> Self::SortKey {\n        self.name.to_owned()\n    }\n}\n\nfn diff_slotmap<I: SlotmapDiffItem>(\n    ui: &mut egui::Ui,\n    this: &mut SlotMap<I::Key, I>,\n    clean: &SlotMap<I::Key, I>,\n) {\n    let mut this_keys: Vec<_> = this.keys().collect();\n    this_keys.sort_by_key(|&k| this[k].sort_key());\n    let mut clean_keys: Vec<_> = clean.keys().collect();\n    clean_keys.sort_by_key(|&k| clean[k].sort_key());\n    let mut any_changed = false;\n    for zip_item in this_keys.into_iter().zip_longest(clean_keys) {\n        match zip_item {\n            EitherOrBoth::Both(this_key, clean_key) => {\n                if this_key != clean_key {\n                    ui.label(\"-\");\n                    any_changed = true;\n                    continue;\n                }\n                let this_item = &this[this_key];\n                let clean_item = &clean[clean_key];\n                if this_item != clean_item {\n                    any_changed = true;\n                    ui.label(format!(\n                        \"{}: {:?}\\n=>\\n{:?}\",\n                        this_item.label(),\n                        this_item,\n                        clean_item\n                    ));\n                }\n            }\n            EitherOrBoth::Left(this_key) => {\n                any_changed = true;\n                ui.label(format!(\"New {}\", this[this_key].label()));\n            }\n            EitherOrBoth::Right(clean_key) => {\n                any_changed = true;\n                ui.label(format!(\"Deleted {}\", clean[clean_key].label()));\n            }\n        }\n    }\n    if any_changed {\n        if ui.button(\"Restore\").clicked() {\n            this.clone_from(clean);\n        }\n    } else {\n        ui.label(\"No changes\");\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/open_process.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        gui::{egui_ui_ext::EguiResponseExt as _, message_dialog::MessageDialog},\n        shell::{msg_fail, msg_if_fail},\n        util::human_size,\n    },\n    egui_extras::{Column, TableBuilder},\n    egui_file_dialog::FileDialog,\n    proc_maps::MapRange,\n    std::{fmt::Write as _, path::PathBuf, process::Command},\n    sysinfo::{ProcessesToUpdate, Signal},\n};\n\ntype MapRanges = Vec<MapRange>;\n\n#[derive(Default)]\npub struct OpenProcessWindow {\n    pub open: WindowOpen,\n    pub sys: sysinfo::System,\n    pub selected_pid: Option<sysinfo::Pid>,\n    pub map_ranges: MapRanges,\n    pid_sort: Sort,\n    addr_sort: Sort,\n    size_sort: Sort,\n    maps_sort_col: MapsSortColumn,\n    pub filters: Filters,\n    modal: Option<Modal>,\n    find: FindState,\n    pub default_meta_path: Option<PathBuf>,\n    use_default_meta_path: bool = true,\n}\n\n#[derive(Default)]\npub struct Filters {\n    pub path: String,\n    pub addr: String,\n    pub proc_name: String,\n    pub perms: PermFilters,\n}\n\n#[derive(Default)]\nstruct FindState {\n    open: bool,\n    input: String,\n    results: Vec<MapFindResults>,\n}\n\nstruct MapFindResults {\n    map: MapRange,\n    offsets: Vec<usize>,\n}\n\n#[derive(Default)]\npub struct PermFilters {\n    read: bool,\n    write: bool,\n    execute: bool,\n}\n\n#[derive(Default, Clone, Copy)]\nenum Sort {\n    #[default]\n    Ascending,\n    Descending,\n}\n\nimpl Sort {\n    fn flip(&mut self) {\n        *self = match *self {\n            Self::Ascending => Self::Descending,\n            Self::Descending => Self::Ascending,\n        }\n    }\n}\n\nfn sort_button(ui: &mut egui::Ui, label: &str, active: bool, sort: Sort) -> egui::Response {\n    let arrow_str = if active {\n        match sort {\n            Sort::Ascending => \"⏶\",\n            Sort::Descending => \"⏷\",\n        }\n    } else {\n        \"=\"\n    };\n    if active {\n        ui.style_mut().visuals.faint_bg_color = egui::Color32::RED;\n    }\n    ui.button(format!(\"{label} {arrow_str}\"))\n}\n\n#[derive(Default, PartialEq, Eq)]\nenum MapsSortColumn {\n    #[default]\n    StartOffset,\n    Size,\n}\n\nenum Modal {\n    RunCommand(RunCommand),\n}\n\nimpl Modal {\n    fn run_command() -> Self {\n        Self::RunCommand(RunCommand {\n            command: String::new(),\n            just_opened: true,\n            file_dialog: FileDialog::new(),\n        })\n    }\n}\n\nstruct RunCommand {\n    command: String,\n    just_opened: bool,\n    file_dialog: FileDialog,\n}\n\nimpl super::Window for OpenProcessWindow {\n    fn ui(\n        &mut self,\n        WinCtx {\n            ui,\n            gui,\n            app,\n            font_size,\n            line_spacing,\n            ..\n        }: WinCtx,\n    ) {\n        ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n        if let Some(modal) = &mut self.modal {\n            let mut close_modal = false;\n            ui.horizontal(|ui| match modal {\n                Modal::RunCommand(run_command) => {\n                    run_command.file_dialog.update(ui.ctx());\n                    ui.label(\"Command\");\n                    if let Some(file_path) = run_command.file_dialog.take_picked() {\n                        let _ = writeln!(&mut run_command.command, \"\\\"{}\\\"\", file_path.display());\n                    }\n                    let re = ui.text_edit_singleline(&mut run_command.command);\n                    if run_command.just_opened {\n                        re.request_focus();\n                        run_command.just_opened = false;\n                    }\n                    let enter = ui.input(|inp| inp.key_pressed(egui::Key::Enter));\n                    match shlex::split(&run_command.command) {\n                        Some(tokens) => {\n                            let mut tokens = tokens.into_iter();\n                            if (ui.button(\"Run\").clicked() || (re.lost_focus() && enter))\n                                && let Some(first) = tokens.next()\n                            {\n                                match Command::new(first).args(tokens).spawn() {\n                                    Ok(child) => {\n                                        let pid = child.id();\n                                        self.selected_pid = Some(sysinfo::Pid::from_u32(pid));\n                                        refresh_proc_maps(\n                                            pid,\n                                            &mut self.map_ranges,\n                                            &mut gui.msg_dialog,\n                                        );\n                                        // Make sure this process is visible for sysinfo to kill/stop/etc.\n                                        self.sys.refresh_processes(ProcessesToUpdate::All, true);\n                                        close_modal = true;\n                                    }\n                                    Err(e) => {\n                                        msg_fail(&e, \"Run command error\", &mut gui.msg_dialog);\n                                    }\n                                }\n                            }\n                        }\n                        None => {\n                            ui.add_enabled(false, egui::Button::new(\"Run\"));\n                        }\n                    }\n                    if ui.button(\"Add file...\").clicked() {\n                        run_command.file_dialog.pick_file();\n                    }\n                    if ui.button(\"Cancel\").clicked() {\n                        close_modal = true;\n                    }\n                }\n            });\n            if close_modal {\n                self.modal = None;\n            }\n            ui.disable();\n        }\n        ui.horizontal(|ui| {\n            match self.selected_pid {\n                None => {\n                    if self.open.just_now() || ui.button(\"Refresh processes\").clicked() {\n                        self.sys.refresh_processes(ProcessesToUpdate::All, true);\n                    }\n                }\n                Some(pid) => {\n                    if ui.button(\"Refresh memory maps\").clicked() {\n                        refresh_proc_maps(pid.as_u32(), &mut self.map_ranges, &mut gui.msg_dialog);\n                    }\n                    if ui\n                        .selectable_label(self.find.open, \"🔍 Find...\")\n                        .on_hover_text(\"Find values across all map ranges\")\n                        .clicked()\n                    {\n                        self.find.open ^= true;\n                    }\n                }\n            }\n            if ui.button(\"Run command...\").clicked() {\n                self.modal = Some(Modal::run_command());\n            }\n            if let Some(path) = &self.default_meta_path {\n                ui.checkbox(\n                    &mut self.use_default_meta_path,\n                    format!(\"Use metafile {}\", path.display()),\n                );\n            }\n        });\n        if let &Some(pid) = &self.selected_pid {\n            if self.find.open {\n                ui.text_edit_singleline(&mut self.find.input);\n                match self.find.input.parse::<u8>() {\n                    Ok(num) => {\n                        if ui.button(\"Find\").clicked() {\n                            self.find.results.clear();\n                            for range in self\n                                .map_ranges\n                                .iter()\n                                .filter(|range| should_retain_range(&self.filters, range))\n                            {\n                                match app.load_proc_memory(\n                                    pid,\n                                    range.start(),\n                                    range.size(),\n                                    range.is_write(),\n                                    &mut gui.msg_dialog,\n                                    font_size,\n                                    line_spacing,\n                                ) {\n                                    Ok(()) => {\n                                        let mut offsets = Vec::new();\n                                        for offset in memchr::memchr_iter(num, &app.data) {\n                                            offsets.push(offset);\n                                        }\n                                        self.find.results.push(MapFindResults {\n                                            map: range.clone(),\n                                            offsets,\n                                        });\n                                    }\n                                    Err(e) => msg_fail(&e, \"Error\", &mut gui.msg_dialog),\n                                }\n                            }\n                        }\n                        if !self.find.results.is_empty() && ui.button(\"Retain\").clicked() {\n                            self.find.results.retain_mut(|result| {\n                                match app.load_proc_memory(\n                                    pid,\n                                    result.map.start(),\n                                    result.map.size(),\n                                    result.map.is_write(),\n                                    &mut gui.msg_dialog,\n                                    font_size,\n                                    line_spacing,\n                                ) {\n                                    Ok(()) => {\n                                        result.offsets.retain(|offset| {\n                                            app.data.get(*offset).is_some_and(|byte| *byte == num)\n                                        });\n                                        !result.offsets.is_empty()\n                                    }\n                                    Err(e) => {\n                                        msg_fail(&e, \"Error\", &mut gui.msg_dialog);\n                                        false\n                                    }\n                                }\n                            });\n                        }\n                    }\n                    Err(e) => {\n                        ui.add_enabled(false, egui::Button::new(\"Find\"))\n                            .on_disabled_hover_text(format!(\"{e}\"));\n                    }\n                }\n\n                let result_count: usize =\n                    self.find.results.iter().map(|res| res.offsets.len()).sum();\n\n                if result_count < 30 {\n                    for (i, result) in self.find.results.iter().enumerate() {\n                        let label = format!(\n                            \"{}..={} ({}) @ {:?}\",\n                            result.map.start(),\n                            result.map.start() + result.map.size(),\n                            result.map.size(),\n                            result.map.filename(),\n                        );\n                        let map_open = app\n                            .src_args\n                            .hard_seek\n                            .is_some_and(|offset| offset == result.map.start());\n                        let _ = ui.selectable_label(map_open, label);\n                        ui.indent(egui::Id::new(\"result_ident\").with(i), |ui| {\n                            for offset in &result.offsets {\n                                ui.horizontal(|ui| {\n                                    if ui.button(format!(\"{offset:X}\")).clicked() {\n                                        if !map_open {\n                                            match app.load_proc_memory(\n                                                pid,\n                                                result.map.start(),\n                                                result.map.size(),\n                                                result.map.is_write(),\n                                                &mut gui.msg_dialog,\n                                                font_size,\n                                                line_spacing,\n                                            ) {\n                                                Ok(()) => {\n                                                    app.search_focus(*offset);\n                                                }\n                                                Err(e) => {\n                                                    msg_fail(&e, \"Error\", &mut gui.msg_dialog);\n                                                }\n                                            }\n                                            if let Some(path) = &self.default_meta_path\n                                                && self.use_default_meta_path\n                                            {\n                                                let result =\n                                                    app.consume_meta_from_file(path.clone(), false);\n                                                msg_if_fail(\n                                                    result,\n                                                    \"Failed to consume metafile\",\n                                                    &mut gui.msg_dialog,\n                                                );\n                                            }\n                                        } else {\n                                            app.search_focus(*offset);\n                                        }\n                                    }\n                                    if map_open {\n                                        let mut s = String::new();\n                                        ui.label(app.data.get(*offset).map_or(\"??\", |off| {\n                                            s = off.to_string();\n                                            s.as_str()\n                                        }));\n                                    }\n                                });\n                            }\n                        });\n                    }\n                }\n\n                ui.label(format!(\"{result_count} Results\"));\n                return;\n            }\n            ui.heading(format!(\"Virtual memory maps for pid {pid}\"));\n            if ui.link(\"Back to process list\").clicked() {\n                self.sys.refresh_processes(ProcessesToUpdate::All, true);\n                self.selected_pid = None;\n            }\n            if let Some(proc) = self.sys.process(pid) {\n                ui.horizontal(|ui| {\n                    if ui.button(\"Stop\").clicked() {\n                        proc.kill_with(Signal::Stop);\n                    }\n                    if ui.button(\"Continue\").clicked() {\n                        proc.kill_with(Signal::Continue);\n                    }\n                    if ui.button(\"Kill\").clicked() {\n                        proc.kill();\n                    }\n                });\n            }\n            let mut filtered = self.map_ranges.clone();\n            TableBuilder::new(ui)\n                .max_scroll_height(400.0)\n                .column(Column::auto())\n                .column(Column::auto())\n                .column(Column::auto())\n                .column(Column::remainder())\n                .striped(true)\n                .resizable(true)\n                .header(20.0, |mut row| {\n                    row.col(|ui| {\n                        ui.horizontal(|ui| {\n                            if sort_button(\n                                ui,\n                                \"\",\n                                self.maps_sort_col == MapsSortColumn::StartOffset,\n                                self.addr_sort,\n                            )\n                            .clicked()\n                            {\n                                self.maps_sort_col = MapsSortColumn::StartOffset;\n                                self.addr_sort.flip();\n                            }\n                            ui.add(\n                                egui::TextEdit::singleline(&mut self.filters.addr)\n                                    .hint_text(\"🔎 Addr\"),\n                            );\n                        });\n                    });\n                    row.col(|ui| {\n                        if sort_button(\n                            ui,\n                            \"size\",\n                            self.maps_sort_col == MapsSortColumn::Size,\n                            self.size_sort,\n                        )\n                        .clicked()\n                        {\n                            self.maps_sort_col = MapsSortColumn::Size;\n                            self.size_sort.flip();\n                        }\n                    });\n                    row.col(|ui| {\n                        ui.add(egui::Label::new(\"r/w/x\").sense(egui::Sense::click())).context_menu(\n                            |ui| {\n                                ui.label(\"Filter\");\n                                ui.separator();\n                                ui.checkbox(&mut self.filters.perms.read, \"Read\");\n                                ui.checkbox(&mut self.filters.perms.write, \"Write\");\n                                ui.checkbox(&mut self.filters.perms.execute, \"Execute\");\n                            },\n                        );\n                    });\n                    row.col(|ui| {\n                        ui.horizontal(|ui| {\n                            ui.add(\n                                egui::TextEdit::singleline(&mut self.filters.path)\n                                    .hint_text(\"🔎 Path\"),\n                            );\n                            if ui.button(\"🗑\").on_hover_text(\"Remove filtered paths\").clicked() {\n                                self.map_ranges.retain(|range| {\n                                    let mut retain = true;\n                                    if let Some(filename) = range.filename()\n                                        && filename\n                                            .display()\n                                            .to_string()\n                                            .contains(&self.filters.path)\n                                    {\n                                        retain = false;\n                                    }\n                                    retain\n                                });\n                                self.filters.path.clear();\n                            }\n                        });\n                    });\n                })\n                .body(|body| {\n                    filtered.retain(|range| should_retain_range(&self.filters, range));\n                    filtered.sort_by(|range1, range2| match self.maps_sort_col {\n                        MapsSortColumn::Size => match self.size_sort {\n                            Sort::Ascending => range1.size().cmp(&range2.size()),\n                            Sort::Descending => range1.size().cmp(&range2.size()).reverse(),\n                        },\n                        MapsSortColumn::StartOffset => match self.addr_sort {\n                            Sort::Ascending => range1.start().cmp(&range2.start()),\n                            Sort::Descending => range1.start().cmp(&range2.start()).reverse(),\n                        },\n                    });\n                    body.rows(20.0, filtered.len(), |mut row| {\n                        let map_range = filtered[row.index()].clone();\n                        // This range is likely open in the editor (range contains hard_seek)\n                        let mut likely_open = false;\n                        if let Some(hard_seek) = app.src_args.hard_seek\n                            && hard_seek >= map_range.start()\n                            && hard_seek < map_range.start() + map_range.size()\n                        {\n                            likely_open = true;\n                        }\n                        row.col(|ui| {\n                            let txt = format!(\"{:X}\", map_range.start());\n                            let mut rich_txt = egui::RichText::new(&txt);\n                            if likely_open {\n                                rich_txt = rich_txt.color(egui::Color32::YELLOW);\n                            }\n                            let mut is_button = false;\n                            let re = if map_range.is_read() {\n                                is_button = true;\n                                ui.add(egui::Button::new(rich_txt))\n                            } else {\n                                ui.add(egui::Label::new(rich_txt).sense(egui::Sense::click()))\n                            };\n                            re.context_menu(|ui| {\n                                if ui.button(\"📋 Copy to clipboard\").clicked() {\n                                    crate::app::set_clipboard_string(\n                                        &mut app.clipboard,\n                                        &mut gui.msg_dialog,\n                                        &txt,\n                                    );\n                                }\n                            });\n                            if re.clicked() && is_button {\n                                msg_if_fail(\n                                    app.load_proc_memory(\n                                        pid,\n                                        map_range.start(),\n                                        map_range.size(),\n                                        map_range.is_write(),\n                                        &mut gui.msg_dialog,\n                                        font_size,\n                                        line_spacing,\n                                    ),\n                                    \"Failed to load process memory\",\n                                    &mut gui.msg_dialog,\n                                );\n                                if let Some(path) = &self.default_meta_path\n                                    && self.use_default_meta_path\n                                {\n                                    let result = app.consume_meta_from_file(path.clone(), false);\n                                    msg_if_fail(\n                                        result,\n                                        \"Failed to consume metafile\",\n                                        &mut gui.msg_dialog,\n                                    );\n                                }\n                                if let Ok(off) = usize::from_str_radix(&self.filters.addr, 16) {\n                                    let off = off - app.src_args.hard_seek.unwrap_or(0);\n                                    app.edit_state.set_cursor(off);\n                                    app.center_view_on_offset(off);\n                                    app.hex_ui.flash_cursor();\n                                }\n                            }\n                        });\n                        row.col(|ui| {\n                            let size = map_range.size();\n                            let txt = size.to_string();\n                            ui.add(egui::Label::new(&txt).sense(egui::Sense::click()))\n                                .on_hover_text_deferred(|| human_size(size))\n                                .context_menu(|ui| {\n                                    if ui.button(\"📋 Copy to clipboard\").clicked() {\n                                        crate::app::set_clipboard_string(\n                                            &mut app.clipboard,\n                                            &mut gui.msg_dialog,\n                                            &txt,\n                                        );\n                                    }\n                                });\n                        });\n                        row.col(|ui| {\n                            ui.label(format!(\n                                \"{}{}{}\",\n                                if map_range.is_read() { \"r\" } else { \"\" },\n                                if map_range.is_write() { \"w\" } else { \"\" },\n                                if map_range.is_exec() { \"x\" } else { \"\" }\n                            ));\n                        });\n                        row.col(|ui| {\n                            let txt = map_range\n                                .filename()\n                                .map(|p| p.display().to_string())\n                                .unwrap_or_default();\n                            ui.add(egui::Label::new(&txt).sense(egui::Sense::click()))\n                                .context_menu(|ui| {\n                                    if ui.button(\"📋 Copy to clipboard\").clicked() {\n                                        crate::app::set_clipboard_string(\n                                            &mut app.clipboard,\n                                            &mut gui.msg_dialog,\n                                            &txt,\n                                        );\n                                    }\n                                });\n                        });\n                    });\n                });\n            ui.separator();\n            ui.label(format!(\n                \"{}/{} maps shown ({})\",\n                filtered.len(),\n                self.map_ranges.len(),\n                human_size(filtered.iter().map(|range| range.size()).sum::<usize>())\n            ));\n        } else {\n            TableBuilder::new(ui)\n                .column(Column::auto())\n                .column(Column::remainder())\n                .resizable(true)\n                .striped(true)\n                .header(20.0, |mut row| {\n                    row.col(|ui| {\n                        if sort_button(ui, \"pid\", true, self.pid_sort).clicked() {\n                            self.pid_sort.flip();\n                        }\n                    });\n                    row.col(|ui| {\n                        ui.add(\n                            egui::TextEdit::singleline(&mut self.filters.proc_name)\n                                .hint_text(\"🔎 Name\"),\n                        );\n                    });\n                })\n                .body(|body| {\n                    let procs = self.sys.processes();\n                    let filt_str = self.filters.proc_name.to_ascii_lowercase();\n                    let mut pids: Vec<&sysinfo::Pid> = procs\n                        .keys()\n                        .filter(|&pid| {\n                            procs[pid]\n                                .name()\n                                .to_string_lossy()\n                                .to_ascii_lowercase()\n                                .contains(&filt_str)\n                        })\n                        .collect();\n                    pids.sort_by(|pid1, pid2| match self.pid_sort {\n                        Sort::Ascending => pid1.cmp(pid2),\n                        Sort::Descending => pid1.cmp(pid2).reverse(),\n                    });\n                    body.rows(20.0, pids.len(), |mut row| {\n                        let pid = pids[row.index()];\n                        row.col(|ui| {\n                            if ui\n                                .selectable_label(Some(*pid) == self.selected_pid, pid.to_string())\n                                .clicked()\n                            {\n                                self.selected_pid = Some(*pid);\n                                match pid.to_string().parse() {\n                                    Ok(pid) => refresh_proc_maps(\n                                        pid,\n                                        &mut self.map_ranges,\n                                        &mut gui.msg_dialog,\n                                    ),\n                                    Err(e) => msg_fail(\n                                        &e,\n                                        \"Failed to parse pid of process\",\n                                        &mut gui.msg_dialog,\n                                    ),\n                                }\n                            }\n                        });\n                        row.col(|ui| {\n                            ui.label(procs[pid].name().to_string_lossy());\n                        });\n                    });\n                });\n        }\n    }\n\n    fn title(&self) -> &str {\n        \"Open process\"\n    }\n}\n\nfn should_retain_range(filters: &Filters, range: &MapRange) -> bool {\n    if filters.perms.read && !range.is_read() {\n        return false;\n    }\n    if filters.perms.write && !range.is_write() {\n        return false;\n    }\n    if filters.perms.execute && !range.is_exec() {\n        return false;\n    }\n    if let Ok(addr) = usize::from_str_radix(&filters.addr, 16)\n        && !(range.start() <= addr && range.start() + range.size() >= addr)\n    {\n        return false;\n    }\n    if filters.path.is_empty() {\n        return true;\n    }\n    match range.filename() {\n        Some(path) => path.display().to_string().contains(&filters.path),\n        None => false,\n    }\n}\n\nfn refresh_proc_maps(pid: u32, win_map_ranges: &mut MapRanges, msg: &mut MessageDialog) {\n    #[cfg_attr(\n        windows,\n        expect(clippy::useless_conversion, reason = \"lossless on windows\")\n    )]\n    match proc_maps::get_process_maps(pid.try_into().expect(\"Couldnt't convert process id\")) {\n        Ok(ranges) => {\n            *win_map_ranges = ranges;\n        }\n        Err(e) => msg_fail(&e, \"Failed to get map ranges for process\", msg),\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/perspectives.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        app::command::Cmd, gui::windows::regions::region_context_menu, meta::PerspectiveKey,\n        shell::msg_if_fail,\n    },\n    egui_extras::{Column, TableBuilder},\n    slotmap::Key as _,\n};\n\n#[derive(Default)]\npub struct PerspectivesWindow {\n    pub open: WindowOpen,\n    pub rename_idx: PerspectiveKey,\n}\nimpl super::Window for PerspectivesWindow {\n    fn ui(&mut self, WinCtx { ui, gui, app, .. }: WinCtx) {\n        ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n        TableBuilder::new(ui)\n            .columns(Column::auto(), 4)\n            .column(Column::remainder())\n            .striped(true)\n            .resizable(true)\n            .header(24.0, |mut row| {\n                row.col(|ui| {\n                    ui.label(\"Name\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Region\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Columns\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Rows\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Flip row order\");\n                });\n            })\n            .body(|body| {\n                let keys: Vec<_> = app.meta_state.meta.low.perspectives.keys().collect();\n                body.rows(20.0, keys.len(), |mut row| {\n                    let idx = row.index();\n                    row.col(|ui| {\n                        if self.rename_idx == keys[idx] {\n                            let re = ui.text_edit_singleline(\n                                &mut app.meta_state.meta.low.perspectives[keys[idx]].name,\n                            );\n                            if re.lost_focus() {\n                                self.rename_idx = PerspectiveKey::null();\n                            } else {\n                                re.request_focus();\n                            }\n                        } else {\n                            let name = &app.meta_state.meta.low.perspectives[keys[idx]].name;\n                            ui.menu_button(name, |ui| {\n                                if ui.button(\"✏ Rename\").clicked() {\n                                    self.rename_idx = keys[idx];\n                                }\n                                if ui.button(\"🗑 Delete\").clicked() {\n                                    app.cmd.push(Cmd::RemovePerspective(keys[idx]));\n                                }\n                                if ui.button(\"Create view\").clicked() {\n                                    app.cmd.push(Cmd::CreateView {\n                                        perspective_key: keys[idx],\n                                        name: name.to_owned(),\n                                    });\n                                }\n                                ui.menu_button(\"Containing views\", |ui| {\n                                    for (view_key, view) in app.meta_state.meta.views.iter() {\n                                        if view.view.perspective == keys[idx]\n                                            && ui.button(&view.name).clicked()\n                                        {\n                                            gui.win.views.open.set(true);\n                                            gui.win.views.selected = view_key;\n                                        }\n                                    }\n                                });\n                                if ui.button(\"Copy name to clipboard\").clicked() {\n                                    let res = app.clipboard.set_text(name);\n                                    msg_if_fail(\n                                        res,\n                                        \"Failed to copy to clipboard\",\n                                        &mut gui.msg_dialog,\n                                    );\n                                }\n                            });\n                        }\n                    });\n                    row.col(|ui| {\n                        let per = &app.meta_state.meta.low.perspectives[keys[idx]];\n                        let reg = &app.meta_state.meta.low.regions[per.region];\n                        let re = ui.link(&reg.name).on_hover_text(&reg.desc);\n                        re.context_menu(|ui| {\n                            region_context_menu(\n                                ui,\n                                reg,\n                                per.region,\n                                &app.meta_state.meta,\n                                &mut app.cmd,\n                                &mut gui.cmd,\n                            );\n                        });\n                        if re.clicked() {\n                            gui.win.regions.open.set(true);\n                            gui.win.regions.selected_key = Some(per.region);\n                        }\n                    });\n                    row.col(|ui| {\n                        let per = &mut app.meta_state.meta.low.perspectives[keys[idx]];\n                        let reg = &app.meta_state.meta.low.regions[per.region];\n                        ui.add(egui::DragValue::new(&mut per.cols).range(1..=reg.region.len()));\n                    });\n                    row.col(|ui| {\n                        let per = &app.meta_state.meta.low.perspectives[keys[idx]];\n                        let reg = &app.meta_state.meta.low.regions[per.region];\n                        let reg_len = reg.region.len();\n                        let cols = per.cols;\n                        let rows = reg_len / cols;\n                        let rem = reg_len % cols;\n                        let rem_str: &str = if rem == 0 {\n                            \"\"\n                        } else {\n                            &format!(\" (rem: {rem})\")\n                        };\n                        ui.label(format!(\"{rows}{rem_str}\"));\n                    });\n                    row.col(|ui| {\n                        ui.checkbox(\n                            &mut app.meta_state.meta.low.perspectives[keys[idx]].flip_row_order,\n                            \"\",\n                        );\n                    });\n                });\n            });\n        ui.separator();\n        ui.menu_button(\"New from region\", |ui| {\n            for (key, region) in app.meta_state.meta.low.regions.iter() {\n                if ui.button(&region.name).clicked() {\n                    app.cmd.push(Cmd::CreatePerspective {\n                        region_key: key,\n                        name: region.name.clone(),\n                    });\n\n                    return;\n                }\n            }\n        });\n    }\n\n    fn title(&self) -> &str {\n        \"Perspectives\"\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/preferences.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        app::{App, backend_command::BackendCmd},\n        config::{self, Config, ProjectDirsExt as _},\n        gui::message_dialog::{Icon, MessageDialog},\n    },\n    egui_colors::{Colorix, tokens::ThemeColor},\n    egui_fontcfg::{CustomFontPaths, FontCfgUi, FontDefsUiMsg},\n    rand::RngExt as _,\n};\n\n#[derive(Default)]\npub struct PreferencesWindow {\n    pub open: WindowOpen,\n    tab: Tab,\n    font_cfg: FontCfgUi,\n    font_defs: egui::FontDefinitions,\n    temp_custom_font_paths: CustomFontPaths,\n}\n\n#[derive(Default, PartialEq)]\nenum Tab {\n    #[default]\n    Video,\n    Style,\n    Fonts,\n}\n\nimpl Tab {\n    fn label(&self) -> &'static str {\n        match self {\n            Self::Video => \"Video\",\n            Self::Style => \"Style\",\n            Self::Fonts => \"Fonts\",\n        }\n    }\n}\n\nimpl super::Window for PreferencesWindow {\n    fn ui(&mut self, WinCtx { ui, gui, app, .. }: WinCtx) {\n        if self.open.just_now() {\n            self.font_defs = ui.ctx().fonts(|f| f.definitions().clone());\n            self.temp_custom_font_paths.clone_from(&app.cfg.custom_font_paths);\n            let _ = egui_fontcfg::load_custom_fonts(\n                &app.cfg.custom_font_paths,\n                &mut self.font_defs.font_data,\n            );\n        }\n        ui.horizontal(|ui| {\n            ui.selectable_value(&mut self.tab, Tab::Video, Tab::Video.label());\n            ui.selectable_value(&mut self.tab, Tab::Style, Tab::Style.label());\n            ui.selectable_value(&mut self.tab, Tab::Fonts, Tab::Fonts.label());\n            ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {\n                if ui.button(\"Open config dir\").clicked() {\n                    match config::project_dirs() {\n                        Some(dirs) => {\n                            if let Err(e) = open::that(dirs.config_dir()) {\n                                gui.msg_dialog.open(\n                                    Icon::Error,\n                                    \"Error opening config dir\",\n                                    e.to_string(),\n                                );\n                            }\n                        }\n                        None => gui.msg_dialog.open(\n                            Icon::Error,\n                            \"Error opening config dir\",\n                            \"Missing config dir\",\n                        ),\n                    }\n                }\n            });\n        });\n        ui.separator();\n        match self.tab {\n            Tab::Video => video_ui(ui, app),\n            Tab::Style => style_ui(app, ui, &mut gui.colorix, &mut gui.msg_dialog),\n            Tab::Fonts => fonts_ui(\n                ui,\n                &mut self.font_cfg,\n                &mut self.font_defs,\n                &mut app.cfg,\n                &mut self.temp_custom_font_paths,\n                &mut gui.msg_dialog,\n            ),\n        }\n    }\n\n    fn title(&self) -> &str {\n        \"Preferences\"\n    }\n}\n\nfn video_ui(ui: &mut egui::Ui, app: &mut App) {\n    if ui.checkbox(&mut app.cfg.vsync, \"Vsync\").clicked() {\n        app.backend_cmd.push(BackendCmd::ApplyVsyncCfg);\n    }\n    ui.horizontal(|ui| {\n        ui.label(\"FPS limit (0 to disable)\");\n        ui.add(egui::DragValue::new(&mut app.cfg.fps_limit));\n        if ui.button(\"Set\").clicked() {\n            app.backend_cmd.push(BackendCmd::ApplyFpsLimit);\n        }\n    });\n}\n\nfn style_ui(\n    app: &mut App,\n    ui: &mut egui::Ui,\n    opt_colorix: &mut Option<Colorix>,\n    msg_dia: &mut MessageDialog,\n) {\n    ui.group(|ui| {\n        let style = &mut app.cfg.style;\n        ui.heading(\"Font sizes\");\n        let mut any_changed = false;\n        ui.horizontal(|ui| {\n            ui.label(\"heading\");\n            any_changed |= ui\n                .add(egui::DragValue::new(&mut style.font_sizes.heading).range(3..=100))\n                .changed();\n        });\n        ui.horizontal(|ui| {\n            ui.label(\"body\");\n            any_changed |= ui\n                .add(egui::DragValue::new(&mut style.font_sizes.body).range(3..=100))\n                .changed();\n        });\n        ui.horizontal(|ui| {\n            ui.label(\"monospace\");\n            any_changed |= ui\n                .add(egui::DragValue::new(&mut style.font_sizes.monospace).range(3..=100))\n                .changed();\n        });\n        ui.horizontal(|ui| {\n            ui.label(\"button\");\n            any_changed |= ui\n                .add(egui::DragValue::new(&mut style.font_sizes.button).range(3..=100))\n                .changed();\n        });\n        ui.horizontal(|ui| {\n            ui.label(\"small\");\n            any_changed |= ui\n                .add(egui::DragValue::new(&mut style.font_sizes.small).range(3..=100))\n                .changed();\n        });\n        if ui.button(\"Reset default\").clicked() {\n            *style = config::Style::default();\n            any_changed = true;\n        }\n        if any_changed {\n            crate::gui::set_font_sizes_ctx(ui.ctx(), style);\n        }\n    });\n    ui.group(|ui| {\n        let colorix = match opt_colorix {\n            Some(colorix) => colorix,\n            None => {\n                if ui.button(\"Activate custom colors\").clicked() {\n                    opt_colorix.insert(Colorix::global(ui.ctx(), egui_colors::utils::EGUI_THEME))\n                } else {\n                    return;\n                }\n            }\n        };\n        let mut clear = false;\n        ui.horizontal(|ui| {\n            colorix.themes_dropdown(ui, None, false);\n            ui.group(|ui| {\n                ui.label(\"light dark toggle\");\n                colorix.light_dark_toggle_button(ui, 30.0);\n            });\n            if ui.button(\"Random theme\").clicked() {\n                let mut rng = rand::rng();\n                *colorix = Colorix::global(\n                    ui.ctx(),\n                    std::array::from_fn(|_| ThemeColor::Custom(rng.random::<[u8; 3]>())),\n                );\n            }\n        });\n        ui.separator();\n        colorix.ui_combo_12(ui, true);\n        if let Some(dirs) = config::project_dirs() {\n            ui.separator();\n            ui.horizontal(|ui| {\n                if ui.button(\"Save\").clicked() {\n                    let data: [[u8; 3]; 12] = colorix.theme().map(|theme| theme.rgb());\n                    if let Err(e) = std::fs::write(dirs.color_theme_path(), data.as_flattened()) {\n                        msg_dia.open(Icon::Error, \"Failed to save theme\", e.to_string());\n                    }\n                };\n                if ui.button(\"Remove custom colors\").clicked() {\n                    if let Err(e) = std::fs::remove_file(dirs.color_theme_path()) {\n                        msg_dia.open(Icon::Error, \"Failed to delete theme file\", e.to_string());\n                    }\n                    clear = true;\n                }\n            });\n        }\n        if clear {\n            ui.ctx().set_visuals(egui::Visuals::dark());\n            *opt_colorix = None;\n        }\n    });\n}\n\nfn fonts_ui(\n    ui: &mut egui::Ui,\n    font_cfg_ui: &mut FontCfgUi,\n    font_defs: &mut egui::FontDefinitions,\n    cfg: &mut Config,\n    temp_custom_font_paths: &mut CustomFontPaths,\n    msg_dia: &mut MessageDialog,\n) {\n    let msg = font_cfg_ui.show(ui, font_defs, Some(temp_custom_font_paths));\n    if matches!(msg, FontDefsUiMsg::SaveRequest) {\n        cfg.font_families = font_defs.families.clone();\n        cfg.custom_font_paths.clone_from(temp_custom_font_paths);\n        msg_dia.open(\n            Icon::Info,\n            \"Config saved\",\n            \"Your font configuration has been saved.\",\n        );\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/regions.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        app::command::{Cmd, CommandQueue},\n        gui::command::{GCmd, GCommandQueue},\n        meta::{Meta, NamedRegion, RegionKey},\n        util::human_size,\n    },\n    egui::TextBuffer as _,\n    egui_extras::{Column, TableBuilder},\n    egui_phosphor::regular as ic,\n};\n\n#[derive(Default)]\npub struct RegionsWindow {\n    pub open: WindowOpen,\n    pub focus_rename: bool,\n    pub selected_key: Option<RegionKey>,\n    pub select_active: bool,\n    pub rename_buffer: Option<String>,\n    pub activate_rename: bool,\n}\n\npub fn region_context_menu(\n    ui: &mut egui::Ui,\n    reg: &NamedRegion,\n    key: RegionKey,\n    meta: &Meta,\n    cmd: &mut CommandQueue,\n    gcmd: &mut GCommandQueue,\n) {\n    ui.menu_button(\"Containing layouts\", |ui| {\n        for (key, layout) in meta.layouts.iter() {\n            if let Some(v) = layout.view_containing_region(&reg.region, meta)\n                && ui.button(&layout.name).clicked()\n            {\n                cmd.push(Cmd::SetLayout(key));\n                cmd.push(Cmd::FocusView(v));\n                cmd.push(Cmd::SetAndFocusCursor(reg.region.begin));\n            }\n        }\n    });\n    ui.menu_button(\"Containing perspectives\", |ui| {\n        for (_per_key, per) in meta.low.perspectives.iter() {\n            if per.region == key && ui.button(&per.name).clicked() {\n                gcmd.push(GCmd::OpenPerspectiveWindow);\n            }\n        }\n    });\n    if ui.button(\"Select\").clicked() {\n        cmd.push(Cmd::SetSelection(reg.region.begin, reg.region.end));\n    }\n    if ui.button(\"Create perspective\").clicked() {\n        cmd.push(Cmd::CreatePerspective {\n            region_key: key,\n            name: reg.name.clone(),\n        });\n    }\n}\n\nimpl super::Window for RegionsWindow {\n    fn ui(&mut self, WinCtx { ui, gui, app, .. }: WinCtx) {\n        ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n        ui.horizontal(|ui| {\n            let button = egui::Button::new(\"Add selection as region\");\n            match app.hex_ui.selection() {\n                Some(sel) => {\n                    if ui.add(button).clicked() {\n                        crate::gui::ops::add_region_from_selection(sel, &mut app.meta_state, self);\n                    }\n                }\n                None => {\n                    ui.add_enabled(false, button);\n                }\n            }\n            if ui.button(\"Add file-sized region\").clicked() {\n                app.meta_state.meta.low.regions.insert(NamedRegion::new(\n                    \"New (file sized)\".into(),\n                    0,\n                    app.data.len().saturating_sub(1),\n                ));\n            }\n        });\n        if let &Some(key) = &self.selected_key {\n            ui.separator();\n            let reg = &mut app.meta_state.meta.low.regions[key];\n            if std::mem::take(&mut self.activate_rename) {\n                self.rename_buffer = Some(reg.name.clone());\n            }\n            let mut unset_rename_buf = false;\n            ui.horizontal(|ui| match &mut self.rename_buffer {\n                Some(buf) => {\n                    let re = ui.text_edit_singleline(buf);\n                    if self.open.just_now() {\n                        self.focus_rename = true;\n                    }\n                    if std::mem::take(&mut self.focus_rename) {\n                        re.request_focus();\n                    }\n                    ui.add_enabled(false, egui::Label::new(\"\"));\n                    if ui.button(ic::X).clicked() {\n                        unset_rename_buf = true;\n                    }\n                    if ui.button(ic::CHECK).clicked()\n                        || ui.input(|inp| inp.key_pressed(egui::Key::Enter))\n                    {\n                        reg.name = buf.take();\n                        self.rename_buffer = None;\n                    }\n                }\n                None => {\n                    ui.heading(&reg.name);\n                    if ui.button(ic::PENCIL).on_hover_text(\"Rename\").clicked() {\n                        self.rename_buffer = Some(reg.name.clone());\n                        self.focus_rename = true;\n                    }\n                }\n            });\n            if unset_rename_buf {\n                self.rename_buffer = None;\n            }\n            ui.horizontal(|ui| {\n                ui.label(\"First byte\");\n                ui.add(egui::DragValue::new(&mut reg.region.begin)).context_menu(|ui| {\n                    if ui.button(\"Set to cursor\").clicked() {\n                        reg.region.begin = app.edit_state.cursor;\n                    }\n                });\n                ui.label(\"Last byte\");\n                ui.add(egui::DragValue::new(&mut reg.region.end)).context_menu(|ui| {\n                    if ui.button(\"Set to cursor\").clicked() {\n                        reg.region.end = app.edit_state.cursor;\n                    }\n                });\n            });\n            ui.label(format!(\n                \"Length: {} ({})\",\n                reg.region.len(),\n                human_size(reg.region.len())\n            ));\n            if self.select_active {\n                app.hex_ui.select_a = Some(reg.region.begin);\n                app.hex_ui.select_b = Some(reg.region.end);\n            }\n            if ui.checkbox(&mut self.select_active, \"Select\").clicked() {\n                app.hex_ui.clear_selections();\n            }\n            if let Some(sel) = app.hex_ui.selection() {\n                if ui.button(\"Set to selection\").clicked() {\n                    reg.region = sel;\n                }\n            } else {\n                ui.add_enabled(false, egui::Button::new(\"Set to selection\"));\n            }\n            if ui.button(\"Reset\").on_hover_text(\"Encompass the whole document\").clicked() {\n                reg.region.begin = 0;\n                reg.region.end = app.data.len() - 1;\n            }\n            ui.label(\"Description\");\n            ui.text_edit_multiline(&mut reg.desc);\n            if ui.button(\"Delete\").clicked() {\n                app.meta_state.meta.low.regions.remove(key);\n                app.remove_dangling();\n                self.selected_key = None;\n            }\n        }\n        ui.separator();\n        TableBuilder::new(ui)\n            .striped(true)\n            .resizable(true)\n            .column(Column::auto())\n            .column(Column::auto())\n            .column(Column::auto())\n            .column(Column::remainder())\n            .header(20.0, |mut header| {\n                header.col(|ui| {\n                    ui.label(\"Name\");\n                });\n                header.col(|ui| {\n                    ui.label(\"First byte\");\n                });\n                header.col(|ui| {\n                    ui.label(\"Last byte\");\n                });\n                header.col(|ui| {\n                    ui.label(\"Length\");\n                });\n            })\n            .body(|body| {\n                let mut keys: Vec<RegionKey> = app.meta_state.meta.low.regions.keys().collect();\n                let mut action = Action::None;\n                keys.sort_by_key(|k| app.meta_state.meta.low.regions[*k].region.begin);\n                body.rows(20.0, keys.len(), |mut row| {\n                    let k = keys[row.index()];\n                    let reg = &app.meta_state.meta.low.regions[k];\n                    row.col(|ui| {\n                        let ctx_menu = |ui: &mut egui::Ui| {\n                            region_context_menu(\n                                ui,\n                                reg,\n                                k,\n                                &app.meta_state.meta,\n                                &mut app.cmd,\n                                &mut gui.cmd,\n                            );\n                        };\n                        let re = ui\n                            .selectable_label(self.selected_key == Some(k), &reg.name)\n                            .on_hover_text(&reg.desc);\n                        re.context_menu(ctx_menu);\n                        if re.clicked() {\n                            self.selected_key = Some(k);\n                        }\n                    });\n                    row.col(|ui| {\n                        let re = ui.link(reg.region.begin.to_string());\n                        re.context_menu(|ui| {\n                            if ui.button(\"Set to cursor\").clicked() {\n                                action = Action::SetRegionBegin {\n                                    key: k,\n                                    begin: app.edit_state.cursor,\n                                };\n                            }\n                        });\n                        if re.clicked() {\n                            action = Action::Goto(reg.region.begin);\n                        }\n                    });\n                    row.col(|ui| {\n                        let re = ui.link(reg.region.end.to_string());\n                        re.context_menu(|ui| {\n                            if ui.button(\"Set to cursor\").clicked() {\n                                action = Action::SetRegionEnd {\n                                    key: k,\n                                    end: app.edit_state.cursor,\n                                };\n                            }\n                        });\n                        if re.clicked() {\n                            action = Action::Goto(reg.region.end);\n                        }\n                    });\n                    row.col(\n                        |ui| match (reg.region.end + 1).checked_sub(reg.region.begin) {\n                            Some(len) => {\n                                ui.label(len.to_string());\n                            }\n                            None => {\n                                ui.label(\"Overflow!\");\n                            }\n                        },\n                    );\n                });\n                match action {\n                    Action::None => {}\n                    Action::Goto(off) => {\n                        app.center_view_on_offset(off);\n                        app.edit_state.set_cursor(off);\n                        app.hex_ui.flash_cursor();\n                    }\n                    Action::SetRegionBegin { key, begin } => {\n                        app.meta_state.meta.low.regions[key].region.begin = begin;\n                    }\n                    Action::SetRegionEnd { key, end } => {\n                        app.meta_state.meta.low.regions[key].region.end = end;\n                    }\n                }\n            });\n    }\n\n    fn title(&self) -> &str {\n        \"Regions\"\n    }\n}\n\nenum Action {\n    None,\n    Goto(usize),\n    SetRegionBegin { key: RegionKey, begin: usize },\n    SetRegionEnd { key: RegionKey, end: usize },\n}\n"
  },
  {
    "path": "src/gui/windows/script_manager.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        app::App,\n        gui::Gui,\n        meta::{ScriptKey, ScriptMap},\n        scripting::exec_lua,\n        shell::msg_if_fail,\n    },\n    egui_code_editor::{CodeEditor, Syntax},\n    mlua::Lua,\n};\n\n#[derive(Default)]\npub struct ScriptManagerWindow {\n    pub open: WindowOpen,\n    selected: Option<ScriptKey>,\n}\n\nimpl super::Window for ScriptManagerWindow {\n    fn ui(\n        &mut self,\n        WinCtx {\n            ui,\n            gui,\n            app,\n            lua,\n            font_size,\n            line_spacing,\n            ..\n        }: WinCtx,\n    ) {\n        let mut scripts = std::mem::take(&mut app.meta_state.meta.scripts);\n        scripts.retain(|key, script| {\n            let mut retain = true;\n            ui.horizontal(|ui| {\n                if app.meta_state.meta.onload_script == Some(key) {\n                    ui.label(\"⚡\").on_hover_text(\"This script executes on document load\");\n                }\n                if ui.selectable_label(self.selected == Some(key), &script.name).clicked() {\n                    self.selected = Some(key);\n                }\n                if ui.button(\"⚡ Execute\").clicked() {\n                    let result = exec_lua(\n                        lua,\n                        &script.content,\n                        app,\n                        gui,\n                        \"\",\n                        Some(key),\n                        font_size,\n                        line_spacing,\n                    );\n                    msg_if_fail(result, \"Failed to execute script\", &mut gui.msg_dialog);\n                }\n                if ui.button(\"Delete\").clicked() {\n                    retain = false;\n                }\n            });\n            retain\n        });\n        if scripts.is_empty() {\n            ui.label(\"There are no saved scripts.\");\n        }\n        if ui.link(\"Open lua editor\").clicked() {\n            gui.win.lua_editor.open.set(true);\n        }\n        ui.separator();\n        self.selected_script_ui(ui, gui, app, lua, &mut scripts, font_size, line_spacing);\n        std::mem::swap(&mut app.meta_state.meta.scripts, &mut scripts);\n    }\n\n    fn title(&self) -> &str {\n        \"Script manager\"\n    }\n}\n\nimpl ScriptManagerWindow {\n    fn selected_script_ui(\n        &mut self,\n        ui: &mut egui::Ui,\n        gui: &mut Gui,\n        app: &mut App,\n        lua: &Lua,\n        scripts: &mut ScriptMap,\n        font_size: u16,\n        line_spacing: u16,\n    ) {\n        let Some(key) = self.selected else {\n            return;\n        };\n        let Some(scr) = scripts.get_mut(key) else {\n            self.selected = None;\n            return;\n        };\n        ui.label(\"Description\");\n        ui.text_edit_multiline(&mut scr.desc);\n        ui.label(\"Code\");\n        egui::ScrollArea::vertical().show(ui, |ui| {\n            CodeEditor::default().with_syntax(Syntax::lua()).show(ui, &mut scr.content);\n        });\n        if ui.button(\"⚡ Execute\").clicked() {\n            let result = exec_lua(\n                lua,\n                &scr.content,\n                app,\n                gui,\n                \"\",\n                Some(key),\n                font_size,\n                line_spacing,\n            );\n            msg_if_fail(result, \"Failed to execute script\", &mut gui.msg_dialog);\n        }\n        if ui.button(\"⚡ Set as onload script\").clicked() {\n            app.meta_state.meta.onload_script = Some(key);\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/structs.rs",
    "content": "use {\n    super::WindowOpen,\n    crate::{\n        meta::Meta,\n        struct_meta_item::{Endian, StructMetaItem, StructPrimitive, StructTy},\n    },\n    egui_code_editor::{CodeEditor, Syntax},\n};\n\n#[derive(Default)]\npub struct StructsWindow {\n    pub open: WindowOpen,\n    struct_text_buf: String,\n    parsed_struct: Option<StructMetaItem>,\n    error_label: String,\n    selected_idx: usize,\n    tab: Tab = Tab::Fields,\n}\n\n#[derive(PartialEq)]\nenum Tab {\n    Fields,\n    AtRow,\n}\n\nimpl super::Window for StructsWindow {\n    fn ui(&mut self, super::WinCtx { ui, app, .. }: super::WinCtx) {\n        if self.open.just_now()\n            && let Some(struct_) = app.meta_state.meta.structs.get(self.selected_idx)\n        {\n            self.struct_text_buf = struct_.src.clone();\n            self.parsed_struct = Some(struct_.clone());\n        }\n        let top_h = ui.available_height() - 32.0;\n        ui.horizontal(|ui| {\n            ui.set_max_height(top_h);\n            ui.vertical(|ui| {\n                self.picker_ui(&app.meta_state.meta, ui);\n            });\n            ui.separator();\n            self.editor_ui(ui);\n            ui.separator();\n            ui.vertical(|ui| {\n                self.parsed_struct_ui(ui, app);\n            });\n        });\n        ui.separator();\n        self.bottom_bar_ui(ui, app);\n    }\n\n    fn title(&self) -> &str {\n        \"Structs\"\n    }\n}\n\nimpl StructsWindow {\n    fn refresh(&mut self, meta: &Meta) {\n        self.struct_text_buf.clear();\n        self.parsed_struct = None;\n        if let Some(struct_) = meta.structs.get(self.selected_idx) {\n            self.struct_text_buf = struct_.src.clone();\n            self.parsed_struct = Some(struct_.clone());\n        }\n    }\n    fn picker_ui(&mut self, meta: &Meta, ui: &mut egui::Ui) {\n        for (i, struct_) in meta.structs.iter().enumerate() {\n            if ui.selectable_label(self.selected_idx == i, &struct_.name).clicked() {\n                self.selected_idx = i;\n                self.struct_text_buf = struct_.src.clone();\n                self.parsed_struct = Some(struct_.clone());\n            }\n        }\n    }\n    fn editor_ui(&mut self, ui: &mut egui::Ui) {\n        let re = CodeEditor::default()\n            .with_syntax(Syntax::rust())\n            .desired_width(300.0)\n            .show(ui, &mut self.struct_text_buf)\n            .response;\n\n        if re.changed() {\n            self.error_label.clear();\n            match structparse::Struct::parse(&self.struct_text_buf) {\n                Ok(struct_) => match StructMetaItem::new(struct_, self.struct_text_buf.clone()) {\n                    Ok(struct_) => {\n                        self.parsed_struct = Some(struct_);\n                    }\n                    Err(e) => {\n                        self.error_label = format!(\"Resolve error: {e}\");\n                    }\n                },\n                Err(e) => {\n                    self.parsed_struct = None;\n                    self.error_label = format!(\"Parse error: {e}\");\n                }\n            }\n        }\n    }\n    fn parsed_struct_ui(&mut self, ui: &mut egui::Ui, app: &mut crate::app::App) {\n        egui::ScrollArea::vertical().auto_shrink(false).show(ui, |ui| {\n            if let Some(struct_) = &mut self.parsed_struct {\n                ui.horizontal(|ui| {\n                    ui.selectable_value(&mut self.tab, Tab::Fields, \"Fields\");\n                    if let Some([row, _col]) = app.row_col_of_cursor()\n                        && let Some(reg) = app.row_region(row)\n                    {\n                        let bm_name = app\n                            .meta_state\n                            .meta\n                            .bookmarks\n                            .iter()\n                            .find(|bm| bm.offset == reg.begin)\n                            .map_or(String::new(), |bm| format!(\" ({})\", bm.label));\n                        ui.selectable_value(\n                            &mut self.tab,\n                            Tab::AtRow,\n                            format!(\"At row {row}{bm_name}\"),\n                        );\n                    }\n                });\n                ui.separator();\n                match self.tab {\n                    Tab::Fields => fields_ui(struct_, ui),\n                    Tab::AtRow => at_row_ui(struct_, ui, app),\n                }\n            }\n            if !self.error_label.is_empty() {\n                let label = egui::Label::new(\n                    egui::RichText::new(&self.error_label).color(egui::Color32::RED),\n                )\n                .extend();\n                ui.add(label);\n            }\n        });\n    }\n    fn bottom_bar_ui(&mut self, ui: &mut egui::Ui, app: &mut crate::app::App) {\n        match &mut self.parsed_struct {\n            Some(struct_) => {\n                let mut del = false;\n                let mut refresh = false;\n                ui.horizontal(|ui| {\n                    if ui.button(\"Save\").clicked() {\n                        struct_.src = self.struct_text_buf.clone();\n                        if let Some(s) =\n                            app.meta_state.meta.structs.iter_mut().find(|s| s.name == struct_.name)\n                        {\n                            *s = struct_.clone();\n                        } else {\n                            app.meta_state.meta.structs.push(struct_.clone());\n                        }\n                    }\n                    if ui.button(\"Delete\").clicked() {\n                        del = true;\n                    }\n                    ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {\n                        if ui.button(\"Restore all\").clicked() {\n                            app.meta_state.meta.structs = app.meta_state.clean_meta.structs.clone();\n                            refresh = true;\n                        }\n                    });\n                });\n                if del {\n                    if self.selected_idx < app.meta_state.meta.structs.len() {\n                        app.meta_state.meta.structs.remove(self.selected_idx);\n                    }\n                    self.selected_idx = self.selected_idx.saturating_sub(1);\n                    self.refresh(&app.meta_state.meta);\n                }\n                if refresh {\n                    self.refresh(&app.meta_state.meta);\n                }\n            }\n            None => {\n                ui.add_enabled(false, egui::Button::new(\"Save\"));\n            }\n        }\n    }\n}\n\nfn fields_ui(struct_: &mut StructMetaItem, ui: &mut egui::Ui) {\n    for (_off, field) in struct_.fields_with_offsets_mut() {\n        ui.horizontal(|ui| {\n            ui.label(format!(\n                \"{}: {} [size: {}]\",\n                field.name,\n                field.ty,\n                field.ty.size()\n            ));\n            let en = field.ty.endian_mut();\n            if ui.checkbox(&mut matches!(en, Endian::Be), en.label()).clicked() {\n                en.toggle();\n            }\n        });\n    }\n}\n\nfn at_row_ui(struct_: &mut StructMetaItem, ui: &mut egui::Ui, app: &mut crate::app::App) {\n    if let Some([row, _]) = app.row_col_of_cursor()\n        && let Some(reg) = app.row_region(row)\n    {\n        for (off, field) in struct_.fields_with_offsets_mut() {\n            ui.horizontal(|ui| {\n                let data_off = reg.begin + off;\n                if ui.link(off.to_string()).clicked() {\n                    app.search_focus(data_off);\n                }\n                ui.label(&field.name);\n                let field_bytes_len = field.ty.size();\n                if let Some(byte_slice) = app.data.get_mut(data_off..data_off + field_bytes_len) {\n                    field_edit_ui(ui, field, byte_slice);\n                } else {\n                    ui.label(\"<out of bounds>\");\n                }\n                if ui.button(\"select\").clicked() {\n                    app.hex_ui.select_a = Some(data_off);\n                    app.hex_ui.select_b = Some(data_off + field.ty.size().saturating_sub(1));\n                }\n            });\n        }\n    }\n}\n\ntrait ToFromBytes: Sized {\n    const LEN: usize = size_of::<Self>();\n    fn from_bytes(bytes: [u8; Self::LEN], endian: Endian) -> Self;\n    fn to_bytes(&self, endian: Endian) -> [u8; Self::LEN];\n}\n\nfn with_bytes_as_primitive<T, F>(bytes: &mut [u8], endian: Endian, mut fun: F)\nwhere\n    T: ToFromBytes,\n    F: FnMut(&mut T),\n    [(); T::LEN]:,\n{\n    if let Ok(arr) = bytes.try_into() {\n        let mut prim = T::from_bytes(arr, endian);\n        fun(&mut prim);\n        bytes.copy_from_slice(prim.to_bytes(endian).as_slice());\n    }\n}\n\nmacro_rules! to_from_impl {\n    ($prim:ty) => {\n        impl ToFromBytes for $prim {\n            fn from_bytes(bytes: [u8; Self::LEN], endian: Endian) -> Self {\n                match endian {\n                    Endian::Le => <$prim>::from_le_bytes(bytes),\n                    Endian::Be => <$prim>::from_be_bytes(bytes),\n                }\n            }\n            fn to_bytes(&self, endian: Endian) -> [u8; Self::LEN] {\n                match endian {\n                    Endian::Le => self.to_le_bytes(),\n                    Endian::Be => self.to_be_bytes(),\n                }\n            }\n        }\n    };\n}\n\nto_from_impl!(i8);\nto_from_impl!(u8);\nto_from_impl!(i16);\nto_from_impl!(u16);\nto_from_impl!(i32);\nto_from_impl!(u32);\nto_from_impl!(i64);\nto_from_impl!(u64);\nto_from_impl!(f32);\nto_from_impl!(f64);\n\nfn field_edit_ui(\n    ui: &mut egui::Ui,\n    field: &crate::struct_meta_item::StructField,\n    byte_slice: &mut [u8],\n) {\n    match &field.ty {\n        StructTy::Primitive { ty, endian } => match ty {\n            StructPrimitive::I8 => {\n                with_bytes_as_primitive(byte_slice, *endian, |num: &mut i8| {\n                    ui.add(egui::DragValue::new(num));\n                });\n            }\n            StructPrimitive::U8 => {\n                with_bytes_as_primitive(byte_slice, *endian, |num: &mut u8| {\n                    ui.add(egui::DragValue::new(num));\n                });\n            }\n            StructPrimitive::I16 => {\n                with_bytes_as_primitive(byte_slice, *endian, |num: &mut i16| {\n                    ui.add(egui::DragValue::new(num));\n                });\n            }\n            StructPrimitive::U16 => {\n                with_bytes_as_primitive(byte_slice, *endian, |num: &mut u16| {\n                    ui.add(egui::DragValue::new(num));\n                });\n            }\n            StructPrimitive::I32 => {\n                with_bytes_as_primitive(byte_slice, *endian, |num: &mut i32| {\n                    ui.add(egui::DragValue::new(num));\n                });\n            }\n            StructPrimitive::U32 => {\n                with_bytes_as_primitive(byte_slice, *endian, |num: &mut u32| {\n                    ui.add(egui::DragValue::new(num));\n                });\n            }\n            StructPrimitive::I64 => {\n                with_bytes_as_primitive(byte_slice, *endian, |num: &mut i64| {\n                    ui.add(egui::DragValue::new(num));\n                });\n            }\n            StructPrimitive::U64 => {\n                with_bytes_as_primitive(byte_slice, *endian, |num: &mut u64| {\n                    ui.add(egui::DragValue::new(num));\n                });\n            }\n            StructPrimitive::F32 => {\n                with_bytes_as_primitive(byte_slice, *endian, |num: &mut f32| {\n                    ui.add(egui::DragValue::new(num));\n                });\n            }\n            StructPrimitive::F64 => {\n                with_bytes_as_primitive(byte_slice, *endian, |num: &mut f64| {\n                    ui.add(egui::DragValue::new(num));\n                });\n            }\n        },\n        StructTy::Array { .. } => {\n            ui.label(\"<array>\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/vars.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::meta::{VarEntry, VarVal},\n    egui::TextBuffer as _,\n    egui_extras::Column,\n};\n\n#[derive(Default)]\npub struct VarsWindow {\n    pub open: WindowOpen,\n    pub new_var_name: String,\n    pub new_val_val: VarVal = VarVal::U64(0),\n}\n\nimpl super::Window for VarsWindow {\n    fn ui(&mut self, WinCtx { ui, app, .. }: WinCtx) {\n        ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n        ui.group(|ui| {\n            ui.label(\"New\");\n            ui.horizontal(|ui| {\n                ui.label(\"Name\");\n                ui.text_edit_singleline(&mut self.new_var_name);\n                ui.label(\"Type\");\n                let sel_txt = var_val_label(&self.new_val_val);\n                egui::ComboBox::new(\"type_select\", \"Type\").selected_text(sel_txt).show_ui(\n                    ui,\n                    |ui| {\n                        ui.selectable_value(&mut self.new_val_val, VarVal::U64(0), \"U64\");\n                        ui.selectable_value(&mut self.new_val_val, VarVal::I64(0), \"I64\");\n                    },\n                );\n                if ui.button(\"Add\").clicked() {\n                    app.meta_state.meta.vars.insert(\n                        self.new_var_name.take(),\n                        VarEntry {\n                            val: self.new_val_val.clone(),\n                            desc: String::new(),\n                        },\n                    );\n                }\n            });\n        });\n        egui_extras::TableBuilder::new(ui)\n            .columns(Column::auto(), 4)\n            .resizable(true)\n            .header(32.0, |mut row| {\n                row.col(|ui| {\n                    ui.label(\"Name\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Type\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Description\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Value\");\n                });\n            })\n            .body(|mut body| {\n                for (key, var_ent) in &mut app.meta_state.meta.vars {\n                    body.row(32.0, |mut row| {\n                        row.col(|ui| {\n                            ui.label(key);\n                        });\n                        row.col(|ui| {\n                            ui.label(var_val_label(&var_ent.val));\n                        });\n                        row.col(|ui| {\n                            ui.text_edit_singleline(&mut var_ent.desc);\n                        });\n                        row.col(|ui| {\n                            match &mut var_ent.val {\n                                VarVal::I64(var) => ui.add(egui::DragValue::new(var)),\n                                VarVal::U64(var) => ui.add(egui::DragValue::new(var)),\n                            };\n                        });\n                    });\n                }\n            });\n    }\n\n    fn title(&self) -> &str {\n        \"Variables\"\n    }\n}\n\nfn var_val_label(var_val: &VarVal) -> &str {\n    match var_val {\n        VarVal::I64(_) => \"i64\",\n        VarVal::U64(_) => \"u64\",\n    }\n}\n"
  },
  {
    "path": "src/gui/windows/views.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        app::{App, command::Cmd},\n        gui::windows::regions::region_context_menu,\n        meta::ViewKey,\n        view::{HexData, TextData, TextKind, ViewKind},\n    },\n    egui::emath::Numeric,\n    egui_extras::{Column, TableBuilder},\n    slotmap::Key as _,\n    std::{hash::Hash, ops::RangeInclusive},\n};\n\n#[derive(Default)]\npub struct ViewsWindow {\n    pub open: WindowOpen,\n    pub selected: ViewKey,\n    rename: bool,\n}\n\nimpl ViewKind {\n    const HEX_NAME: &'static str = \"Hex\";\n    const DEC_NAME: &'static str = \"Decimal\";\n    const TEXT_NAME: &'static str = \"Text\";\n    const BLOCK_NAME: &'static str = \"Block\";\n    fn name(&self) -> &'static str {\n        match *self {\n            Self::Hex(_) => Self::HEX_NAME,\n            Self::Dec(_) => Self::DEC_NAME,\n            Self::Text(_) => Self::TEXT_NAME,\n            Self::Block => Self::BLOCK_NAME,\n        }\n    }\n}\n\npub const MIN_FONT_SIZE: u16 = 5;\npub const MAX_FONT_SIZE: u16 = 256;\n\nimpl super::Window for ViewsWindow {\n    fn ui(\n        &mut self,\n        WinCtx {\n            ui,\n            gui,\n            app,\n            font_size,\n            line_spacing,\n            font,\n            ..\n        }: WinCtx,\n    ) {\n        ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n        if self.open.just_now() &&\n           // Don't override selected key if there already is one\n           // For example, it could be set by the context menu \"view properties\".\n           self.selected.is_null() &&\n           let Some(view_key) = app.hex_ui.focused_view\n        {\n            self.selected = view_key;\n        }\n        let mut removed_idx = None;\n        if app.meta_state.meta.views.is_empty() {\n            ui.label(\"No views\");\n            new_from_perspective_button(ui, app);\n            return;\n        }\n        TableBuilder::new(ui)\n            .columns(Column::auto(), 3)\n            .column(Column::remainder())\n            .resizable(true)\n            .header(24.0, |mut row| {\n                row.col(|ui| {\n                    ui.label(\"Name\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Kind\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Perspective\");\n                });\n                row.col(|ui| {\n                    ui.label(\"Region\");\n                });\n            })\n            .body(|body| {\n                let keys: Vec<ViewKey> = app.meta_state.meta.views.keys().collect();\n                body.rows(20.0, keys.len(), |mut row| {\n                    let view_key = keys[row.index()];\n                    let view = &app.meta_state.meta.views[view_key];\n                    row.col(|ui| {\n                        let ctx_menu = |ui: &mut egui::Ui| {\n                            ui.menu_button(\"Containing layouts\", |ui| {\n                                for (key, layout) in app.meta_state.meta.layouts.iter() {\n                                    if layout.contains_view(view_key)\n                                        && ui.button(&layout.name).clicked()\n                                    {\n                                        App::switch_layout(\n                                            &mut app.hex_ui,\n                                            &app.meta_state.meta,\n                                            key,\n                                        );\n                                        app.hex_ui.focused_view = Some(view_key);\n                                    }\n                                }\n                            });\n                        };\n                        let re = ui.selectable_label(view_key == self.selected, &view.name);\n                        re.context_menu(ctx_menu);\n                        if re.clicked() {\n                            self.selected = view_key;\n                        }\n                        if re.double_clicked() {\n                            App::focus_first_view_of_key(\n                                &mut app.hex_ui,\n                                &app.meta_state.meta,\n                                view_key,\n                            );\n                        }\n                    });\n                    row.col(|ui| {\n                        ui.label(egui::RichText::new(view.view.kind.name()).code());\n                    });\n                    row.col(|ui| {\n                        if ui\n                            .link(&app.meta_state.meta.low.perspectives[view.view.perspective].name)\n                            .clicked()\n                        {\n                            gui.win.perspectives.open.set(true);\n                        }\n                    });\n                    row.col(|ui| {\n                        let per = &app.meta_state.meta.low.perspectives[view.view.perspective];\n                        let reg = &app.meta_state.meta.low.regions[per.region];\n                        let ctx_menu = |ui: &mut egui::Ui| {\n                            region_context_menu(\n                                ui,\n                                reg,\n                                per.region,\n                                &app.meta_state.meta,\n                                &mut app.cmd,\n                                &mut gui.cmd,\n                            );\n                        };\n                        let re = ui.link(&reg.name).on_hover_text(&reg.desc);\n                        re.context_menu(ctx_menu);\n                        if re.clicked() {\n                            gui.win.regions.open.set(true);\n                            gui.win.regions.selected_key = Some(per.region);\n                        }\n                    });\n                });\n            });\n        ui.separator();\n        new_from_perspective_button(ui, app);\n        ui.separator();\n        if let Some(view) = app.meta_state.meta.views.get_mut(self.selected) {\n            ui.horizontal(|ui| {\n                if self.rename {\n                    if ui\n                        .add(egui::TextEdit::singleline(&mut view.name).desired_width(150.0))\n                        .lost_focus()\n                    {\n                        self.rename = false;\n                    }\n                } else {\n                    ui.heading(&view.name);\n                }\n                if ui.button(\"✏\").on_hover_text(\"Rename\").clicked() {\n                    self.rename ^= true;\n                }\n                if view_combo(\n                    egui::Id::new(\"view_combo\"),\n                    &mut view.view.kind,\n                    ui,\n                    font_size,\n                    line_spacing,\n                ) {\n                    view.view.adjust_state_to_kind();\n                }\n            });\n            egui::ComboBox::new(\"new_perspective_combo\", \"Perspective\")\n                .selected_text(&app.meta_state.meta.low.perspectives[view.view.perspective].name)\n                .show_ui(ui, |ui| {\n                    for k in app.meta_state.meta.low.perspectives.keys() {\n                        if ui\n                            .selectable_label(\n                                k == view.view.perspective,\n                                &app.meta_state.meta.low.perspectives[k].name,\n                            )\n                            .clicked()\n                        {\n                            view.view.perspective = k;\n                        }\n                    }\n                });\n            ui.group(|ui| {\n                let mut adjust_block_size = false;\n                match &mut view.view.kind {\n                    ViewKind::Hex(HexData { font_size, .. })\n                    | ViewKind::Dec(HexData { font_size, .. })\n                    | ViewKind::Text(TextData { font_size, .. }) => {\n                        ui.horizontal(|ui| {\n                            ui.label(\"Font size\");\n                            if ui\n                                .add(\n                                    egui::DragValue::new(font_size)\n                                        .range(MIN_FONT_SIZE..=MAX_FONT_SIZE),\n                                )\n                                .changed()\n                            {\n                                adjust_block_size = true;\n                            };\n                        });\n                        if let ViewKind::Text(text) = &mut view.view.kind {\n                            let mut changed = false;\n                            egui::ComboBox::new(egui::Id::new(\"text_combo\"), \"Text kind\")\n                                .selected_text(text.text_kind.name())\n                                .show_ui(ui, |ui| {\n                                    changed |= ui\n                                        .selectable_value(\n                                            &mut text.text_kind,\n                                            TextKind::Ascii,\n                                            TextKind::Ascii.name(),\n                                        )\n                                        .clicked();\n                                    changed |= ui\n                                        .selectable_value(\n                                            &mut text.text_kind,\n                                            TextKind::Utf16Le,\n                                            TextKind::Utf16Le.name(),\n                                        )\n                                        .clicked();\n                                    changed |= ui\n                                        .selectable_value(\n                                            &mut text.text_kind,\n                                            TextKind::Utf16Be,\n                                            TextKind::Utf16Be.name(),\n                                        )\n                                        .clicked();\n                                });\n                            if changed {\n                                view.view.bytes_per_block = text.text_kind.bytes_needed();\n                            }\n                            ui.label(\"Ascii offset\");\n                            ui.add(egui::DragValue::new(&mut text.offset));\n                        }\n                    }\n                    ViewKind::Block => {}\n                }\n                if adjust_block_size {\n                    // We expect line spacing to be a positive integer that fits into u16\n                    #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]\n                    if let ViewKind::Text(data) = &mut view.view.kind {\n                        data.line_spacing = font.line_spacing(u32::from(data.font_size)) as u16;\n                    }\n                    view.view.adjust_block_size();\n                }\n                ui.horizontal(|ui| {\n                    labelled_drag(ui, \"col w\", &mut view.view.col_w, 1..=128);\n                    labelled_drag(ui, \"row h\", &mut view.view.row_h, 1..=128);\n                });\n                labelled_drag(\n                    ui,\n                    \"bytes per block\",\n                    &mut view.view.bytes_per_block,\n                    1..=64,\n                );\n            });\n            if ui.button(\"Delete\").clicked() {\n                removed_idx = Some(self.selected);\n            }\n        }\n        if let Some(rem_key) = removed_idx {\n            app.meta_state.meta.remove_view(rem_key);\n            app.hex_ui.focused_view = None;\n        }\n    }\n\n    fn title(&self) -> &str {\n        \"Views\"\n    }\n}\n\nfn new_from_perspective_button(ui: &mut egui::Ui, app: &mut App) {\n    ui.menu_button(\"New from perspective\", |ui| {\n        for (key, perspective) in app.meta_state.meta.low.perspectives.iter() {\n            if ui.button(&perspective.name).clicked() {\n                app.cmd.push(Cmd::CreateView {\n                    perspective_key: key,\n                    name: perspective.name.to_owned(),\n                });\n            }\n        }\n    });\n}\n\n/// Returns whether the value was changed\nfn view_combo(\n    id: impl Hash,\n    kind: &mut ViewKind,\n    ui: &mut egui::Ui,\n    font_size: u16,\n    line_spacing: u16,\n) -> bool {\n    let mut changed = false;\n    egui::ComboBox::new(id, \"kind\").selected_text(kind.name()).show_ui(ui, |ui| {\n        if ui\n            .selectable_label(kind.name() == ViewKind::HEX_NAME, ViewKind::HEX_NAME)\n            .clicked()\n        {\n            *kind = ViewKind::Hex(HexData::with_font_size(font_size));\n            changed = true;\n        }\n        if ui\n            .selectable_label(kind.name() == ViewKind::DEC_NAME, ViewKind::DEC_NAME)\n            .clicked()\n        {\n            *kind = ViewKind::Dec(HexData::with_font_size(font_size));\n            changed = true;\n        }\n        if ui\n            .selectable_label(kind.name() == ViewKind::TEXT_NAME, ViewKind::TEXT_NAME)\n            .clicked()\n        {\n            *kind = ViewKind::Text(TextData::with_font_info(line_spacing, font_size));\n            changed = true;\n        }\n        if ui\n            .selectable_label(kind.name() == ViewKind::BLOCK_NAME, ViewKind::BLOCK_NAME)\n            .clicked()\n        {\n            *kind = ViewKind::Block;\n            changed = true;\n        }\n    });\n    changed\n}\n\nfn labelled_drag<T: Numeric>(\n    ui: &mut egui::Ui,\n    label: &str,\n    val: &mut T,\n    range: impl Into<Option<RangeInclusive<T>>>,\n) -> egui::Response {\n    ui.horizontal(|ui| {\n        ui.label(label);\n        let mut dv = egui::DragValue::new(val);\n        if let Some(range) = range.into() {\n            dv = dv.range(range);\n        }\n        ui.add(dv)\n    })\n    .inner\n}\n"
  },
  {
    "path": "src/gui/windows/zero_partition.rs",
    "content": "use {\n    super::{WinCtx, WindowOpen},\n    crate::{\n        gui::egui_ui_ext::EguiResponseExt as _, meta::region::Region, shell::msg_if_fail,\n        util::human_size,\n    },\n    egui_extras::{Column, TableBuilder},\n};\n\npub struct ZeroPartition {\n    pub open: WindowOpen,\n    threshold: usize,\n    regions: Vec<Region>,\n    reload: bool,\n}\n\nimpl Default for ZeroPartition {\n    fn default() -> Self {\n        Self {\n            open: Default::default(),\n            threshold: 4096,\n            regions: Default::default(),\n            reload: false,\n        }\n    }\n}\n\nimpl super::Window for ZeroPartition {\n    fn ui(&mut self, WinCtx { ui, app, gui, .. }: WinCtx) {\n        ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);\n        ui.horizontal(|ui| {\n            ui.label(\"Threshold\");\n            ui.add(egui::DragValue::new(&mut self.threshold));\n            if ui.button(\"Go\").clicked() {\n                if self.reload {\n                    msg_if_fail(app.reload(), \"Failed to reload\", &mut gui.msg_dialog);\n                }\n                self.regions = zero_partition(&app.data, self.threshold);\n            }\n            ui.checkbox(&mut self.reload, \"reload\")\n                .on_hover_text(\"Auto reload data before partitioning\");\n            if !self.regions.is_empty() {\n                ui.label(format!(\"{} results\", self.regions.len()));\n            }\n        });\n        if self.regions.is_empty() {\n            return;\n        }\n        ui.separator();\n        TableBuilder::new(ui)\n            .columns(Column::auto(), 4)\n            .auto_shrink([false, true])\n            .striped(true)\n            .header(24.0, |mut row| {\n                row.col(|ui| {\n                    if ui.button(\"begin\").clicked() {\n                        self.regions.sort_by_key(|r| r.begin);\n                    }\n                });\n                row.col(|ui| {\n                    if ui.button(\"end\").clicked() {\n                        self.regions.sort_by_key(|r| r.end);\n                    }\n                });\n                row.col(|ui| {\n                    if ui.button(\"size\").clicked() {\n                        self.regions.sort_by_key(|r| r.len());\n                    }\n                });\n            })\n            .body(|body| {\n                body.rows(24.0, self.regions.len(), |mut row| {\n                    let reg = &self.regions[row.index()];\n                    if reg.contains(app.edit_state.cursor) {\n                        row.set_selected(true);\n                    }\n                    row.col(|ui| {\n                        if ui\n                            .link(reg.begin.to_string())\n                            .on_hover_text_deferred(|| human_size(reg.begin))\n                            .clicked()\n                        {\n                            app.search_focus(reg.begin);\n                        }\n                    });\n                    row.col(|ui| {\n                        if ui\n                            .link(reg.end.to_string())\n                            .on_hover_text_deferred(|| human_size(reg.end))\n                            .clicked()\n                        {\n                            app.search_focus(reg.end);\n                        }\n                    });\n                    row.col(|ui| {\n                        ui.label(reg.len().to_string())\n                            .on_hover_text_deferred(|| human_size(reg.len()));\n                    });\n                    row.col(|ui| {\n                        if ui.button(\"Select\").clicked() {\n                            app.hex_ui.select_a = Some(reg.begin);\n                            app.hex_ui.select_b = Some(reg.end);\n                        }\n                    });\n                });\n            });\n    }\n\n    fn title(&self) -> &str {\n        \"Zero partition\"\n    }\n}\n\nfn zero_partition(data: &[u8], threshold: usize) -> Vec<Region> {\n    if data.is_empty() {\n        return Vec::new();\n    }\n    let mut regions = Vec::new();\n    let mut reg = Region { begin: 0, end: 0 };\n    let mut in_zero = if threshold == 1 { data[0] == 0 } else { false };\n    let mut zero_counter = 0;\n    for (i, &byte) in data.iter().enumerate() {\n        if byte == 0 {\n            zero_counter += 1;\n            if zero_counter == threshold {\n                if i > threshold && !in_zero {\n                    reg.end = i.saturating_sub(threshold);\n                    regions.push(reg);\n                }\n                in_zero = true;\n            }\n        } else {\n            zero_counter = 0;\n            if in_zero {\n                in_zero = false;\n                reg.begin = i;\n            }\n        }\n    }\n    if !in_zero {\n        reg.end = data.len() - 1;\n        regions.push(reg);\n    }\n    regions\n}\n\n#[test]\nfn test_zero_partition() {\n    assert_eq!(\n        zero_partition(&[1, 1, 0, 0, 0, 1, 2, 3], 3),\n        vec![Region { begin: 0, end: 1 }, Region { begin: 5, end: 7 }]\n    );\n    assert_eq!(\n        zero_partition(&[1, 1, 0, 0, 0, 1, 2, 3], 4),\n        vec![Region { begin: 0, end: 7 }]\n    );\n    assert_eq!(\n        zero_partition(\n            &[0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1],\n            3\n        ),\n        vec![\n            Region { begin: 0, end: 4 },\n            Region { begin: 11, end: 14 },\n            Region { begin: 18, end: 18 }\n        ]\n    );\n    assert_eq!(\n        zero_partition(\n            &[0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 1],\n            1\n        ),\n        vec![\n            Region { begin: 1, end: 4 },\n            Region { begin: 11, end: 14 },\n            Region { begin: 18, end: 18 }\n        ]\n    );\n    // head and tail that exceed threshold\n    assert_eq!(\n        zero_partition(\n            &[\n                0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 0, 0, 0, 0, 4, 5, 6, 0, 0, 0, 0, 0, 0\n            ],\n            4\n        ),\n        vec![Region { begin: 10, end: 12 }, Region { begin: 17, end: 19 },]\n    );\n}\n"
  },
  {
    "path": "src/gui/windows.rs",
    "content": "pub use self::{\n    file_diff_result::FileDiffResultWindow,\n    lua_console::{ConMsg, LuaConsoleWindow},\n    regions::{RegionsWindow, region_context_menu},\n};\nuse {\n    self::{\n        about::AboutWindow, bookmarks::BookmarksWindow, external_command::ExternalCommandWindow,\n        find_dialog::FindDialog, find_memory_pointers::FindMemoryPointersWindow,\n        layouts::LayoutsWindow, lua_help::LuaHelpWindow, lua_watch::LuaWatchWindow,\n        meta_diff::MetaDiffWindow, open_process::OpenProcessWindow,\n        perspectives::PerspectivesWindow, preferences::PreferencesWindow,\n        script_manager::ScriptManagerWindow, structs::StructsWindow, vars::VarsWindow,\n        views::ViewsWindow, zero_partition::ZeroPartition,\n    },\n    super::Gui,\n    crate::app::App,\n    egui_sf2g::sf2g::graphics::Font,\n    lua_editor::LuaEditorWindow,\n};\n\nmod about;\nmod bookmarks;\npub mod debug;\nmod external_command;\nmod file_diff_result;\nmod find_dialog;\nmod find_memory_pointers;\nmod layouts;\nmod lua_console;\nmod lua_editor;\nmod lua_help;\nmod lua_watch;\nmod meta_diff;\nmod open_process;\nmod perspectives;\nmod preferences;\nmod regions;\nmod script_manager;\nmod structs;\nmod vars;\nmod views;\nmod zero_partition;\n\n#[derive(Default)]\npub struct Windows {\n    pub layouts: LayoutsWindow,\n    pub views: ViewsWindow,\n    pub regions: RegionsWindow,\n    pub bookmarks: BookmarksWindow,\n    pub find: FindDialog,\n    pub perspectives: PerspectivesWindow,\n    pub file_diff_result: FileDiffResultWindow,\n    pub open_process: OpenProcessWindow,\n    pub find_memory_pointers: FindMemoryPointersWindow,\n    pub external_command: ExternalCommandWindow,\n    pub preferences: PreferencesWindow,\n    pub about: AboutWindow,\n    pub vars: VarsWindow,\n    pub lua_editor: LuaEditorWindow,\n    pub lua_help: LuaHelpWindow,\n    pub lua_console: LuaConsoleWindow,\n    pub lua_watch: Vec<LuaWatchWindow>,\n    pub script_manager: ScriptManagerWindow,\n    pub meta_diff: MetaDiffWindow,\n    pub zero_partition: ZeroPartition,\n    pub structs: StructsWindow,\n}\n\n#[derive(Default)]\npub(crate) struct WindowOpen {\n    open: bool,\n    just_opened: bool,\n}\n\nimpl WindowOpen {\n    /// Open if closed, close if opened\n    pub fn toggle(&mut self) {\n        self.open ^= true;\n        if self.open {\n            self.just_opened = true;\n        }\n    }\n    /// Wheter the window is open\n    fn is(&self) -> bool {\n        self.open\n    }\n    /// Set whether the window is open\n    pub fn set(&mut self, open: bool) {\n        if !self.open && open {\n            self.just_opened = true;\n        }\n        self.open = open;\n    }\n    /// Whether the window was opened just now (this frame)\n    fn just_now(&self) -> bool {\n        self.just_opened\n    }\n}\n\nstruct WinCtx<'a> {\n    ui: &'a mut egui::Ui,\n    gui: &'a mut Gui,\n    app: &'a mut App,\n    lua: &'a mlua::Lua,\n    font_size: u16,\n    line_spacing: u16,\n    font: &'a Font,\n}\n\ntrait Window {\n    fn ui(&mut self, ctx: WinCtx);\n    fn title(&self) -> &str;\n}\n\nimpl Windows {\n    pub(crate) fn update(\n        ctx: &egui::Context,\n        gui: &mut Gui,\n        app: &mut App,\n        lua: &mlua::Lua,\n        font_size: u16,\n        line_spacing: u16,\n        font: &Font,\n    ) {\n        let mut open;\n        macro_rules! windows {\n            ($($field:ident,)*) => {\n                $(\n                    let mut win = std::mem::take(&mut gui.win.$field);\n                    open = win.open.is();\n                    egui::Window::new(win.title()).open(&mut open).show(ctx, |ui| win.ui(WinCtx{ ui, gui, app, lua, font_size, line_spacing, font }));\n                    win.open.just_opened = false;\n                    if !open {\n                        win.open.set(false);\n                    }\n                    std::mem::swap(&mut gui.win.$field, &mut win);\n                )*\n            };\n        }\n        windows!(\n            find,\n            regions,\n            bookmarks,\n            layouts,\n            views,\n            vars,\n            perspectives,\n            file_diff_result,\n            meta_diff,\n            open_process,\n            find_memory_pointers,\n            external_command,\n            preferences,\n            lua_editor,\n            lua_help,\n            lua_console,\n            script_manager,\n            about,\n            zero_partition,\n            structs,\n        );\n\n        let mut watch_windows = std::mem::take(&mut gui.win.lua_watch);\n        let mut i = 0;\n        watch_windows.retain_mut(|win| {\n            let mut retain = true;\n            egui::Window::new(&win.name)\n                .id(egui::Id::new(\"watch_w\").with(i))\n                .open(&mut retain)\n                .show(ctx, |ui| {\n                    win.ui(WinCtx {\n                        ui,\n                        gui,\n                        app,\n                        lua,\n                        font_size,\n                        line_spacing,\n                        font,\n                    });\n                });\n            i += 1;\n            retain\n        });\n        std::mem::swap(&mut gui.win.lua_watch, &mut watch_windows);\n    }\n    pub fn add_lua_watch_window(&mut self) {\n        self.lua_watch.push(LuaWatchWindow::default());\n    }\n}\n"
  },
  {
    "path": "src/gui.rs",
    "content": "pub use self::windows::ConMsg;\nuse {\n    self::{\n        command::GCommandQueue, file_ops::FileOps, inspect_panel::InspectPanel,\n        message_dialog::MessageDialog, windows::Windows,\n    },\n    crate::{\n        app::App,\n        config::Style,\n        meta::{\n            Bookmark,\n            value_type::{self, ValueType},\n        },\n        view::{ViewportScalar, ViewportVec},\n    },\n    egui::{\n        FontFamily::{self, Proportional},\n        FontId, Panel,\n        TextStyle::{Body, Button, Heading, Monospace, Small},\n        Window,\n    },\n    egui_colors::Colorix,\n    egui_sf2g::{\n        SfEgui,\n        sf2g::graphics::{Font, RenderWindow},\n    },\n    gamedebug_core::{IMMEDIATE, PERSISTENT},\n    mlua::Lua,\n    root_ctx_menu::ContextMenu,\n    std::{\n        any::TypeId,\n        collections::{HashMap, HashSet},\n    },\n};\n\nmod bottom_panel;\npub mod command;\npub mod dialogs;\nmod egui_ui_ext;\npub mod file_ops;\npub mod inspect_panel;\npub mod message_dialog;\nmod ops;\npub mod root_ctx_menu;\npub mod selection_menu;\npub mod top_menu;\nmod top_panel;\npub mod windows;\n\nconst BOOK_URL: &str = \"https://crumblingstatue.github.io/hexerator-book/0.4.0\";\n\ntype Dialogs = HashMap<TypeId, Box<dyn Dialog>>;\n\npub type HighlightSet = HashSet<usize>;\n\n#[derive(Default)]\npub struct Gui {\n    pub inspect_panel: InspectPanel,\n    pub dialogs: Dialogs,\n    pub context_menu: Option<ContextMenu>,\n    pub msg_dialog: MessageDialog,\n    /// What to highlight in addition to selection. Can be updated by various actions that want to highlight stuff\n    pub highlight_set: HighlightSet,\n    pub cmd: GCommandQueue,\n    pub fileops: FileOps,\n    pub win: Windows,\n    pub colorix: Option<Colorix>,\n    pub show_quick_scroll_popup: bool,\n}\n\npub trait Dialog {\n    fn title(&self) -> &str;\n    /// Do the ui for this dialog. Returns whether to keep this dialog open.\n    fn ui(\n        &mut self,\n        ui: &mut egui::Ui,\n        app: &mut App,\n        gui: &mut Gui,\n        lua: &Lua,\n        font_size: u16,\n        line_spacing: u16,\n    ) -> bool;\n    /// Called when dialog is opened. Can be used to set just-opened flag, etc.\n    fn on_open(&mut self) {}\n    fn has_close_button(&self) -> bool {\n        false\n    }\n}\n\nimpl Gui {\n    pub fn add_dialog<D: Dialog + 'static>(gui_dialogs: &mut Dialogs, mut dialog: D) {\n        dialog.on_open();\n        gui_dialogs.insert(TypeId::of::<D>(), Box::new(dialog));\n    }\n}\n\n/// The bool indicates whether the application should continue running\npub fn do_egui(\n    sf_egui: &mut SfEgui,\n    gui: &mut Gui,\n    app: &mut App,\n    mouse_pos: ViewportVec,\n    lua: &Lua,\n    rwin: &mut RenderWindow,\n    font_size: u16,\n    line_spacing: u16,\n    font: &Font,\n) -> anyhow::Result<(egui_sf2g::DrawInput, bool)> {\n    let di = sf_egui.run(rwin, |_rwin, ui| {\n        let mut open = IMMEDIATE.enabled() || PERSISTENT.enabled();\n        let was_open = open;\n        if open {\n            app.imm_debug_fun();\n        }\n        Window::new(\"Debug\").open(&mut open).show(ui, windows::debug::ui);\n        if was_open && !open {\n            IMMEDIATE.toggle();\n            PERSISTENT.toggle();\n        }\n        gui.msg_dialog.show(ui, &mut app.clipboard, &mut app.cmd);\n        app.flush_command_queue(gui, lua, font_size, line_spacing);\n        Windows::update(ui, gui, app, lua, font_size, line_spacing, font);\n\n        // Context menu\n        if let Some(menu) = gui.context_menu.take()\n            && root_ctx_menu::show(&menu, ui, app, gui)\n        {\n            gui.context_menu = Some(menu);\n        }\n        // Panels\n        let top_re = Panel::top(\"top_panel\").show_inside(ui, |ui| {\n            top_panel::ui(ui, gui, app, lua, font_size, line_spacing);\n        });\n        let bot_re = Panel::bottom(\"bottom_panel\")\n            .show_inside(ui, |ui| bottom_panel::ui(ui, app, mouse_pos, gui));\n        let right_re = Panel::right(\"right_panel\")\n            .show_inside(ui, |ui| inspect_panel::ui(ui, app, gui, mouse_pos))\n            .response;\n        let padding = 2;\n        app.hex_ui.hex_iface_rect.x = padding;\n        #[expect(\n            clippy::cast_possible_truncation,\n            reason = \"Window size can't exceed i16\"\n        )]\n        {\n            app.hex_ui.hex_iface_rect.y = top_re.response.rect.bottom() as ViewportScalar + padding;\n        }\n        #[expect(\n            clippy::cast_possible_truncation,\n            reason = \"Window size can't exceed i16\"\n        )]\n        {\n            app.hex_ui.hex_iface_rect.w = right_re.rect.left() as ViewportScalar - padding * 2;\n        }\n        #[expect(\n            clippy::cast_possible_truncation,\n            reason = \"Window size can't exceed i16\"\n        )]\n        {\n            app.hex_ui.hex_iface_rect.h = (bot_re.response.rect.top() as ViewportScalar\n                - app.hex_ui.hex_iface_rect.y)\n                - padding * 2;\n        }\n        let mut dialogs = std::mem::take(&mut gui.dialogs);\n        dialogs.retain(|_k, dialog| {\n            let mut retain = true;\n            let mut win = Window::new(dialog.title())\n                .collapsible(false)\n                .resizable(false)\n                .anchor(egui::Align2::CENTER_CENTER, [0., 0.]);\n            let mut open = true;\n            if dialog.has_close_button() {\n                win = win.open(&mut open);\n            }\n            win.show(ui, |ui| {\n                retain = dialog.ui(ui, app, gui, lua, font_size, line_spacing);\n            });\n            if !open {\n                retain = false;\n            }\n            retain\n        });\n        std::mem::swap(&mut gui.dialogs, &mut dialogs);\n        // File dialog\n        gui.fileops.update(\n            ui,\n            app,\n            &mut gui.msg_dialog,\n            &mut gui.win.file_diff_result,\n            font_size,\n            line_spacing,\n        );\n    })?;\n    Ok((di, true))\n}\n\npub fn set_font_sizes_ctx(ctx: &egui::Context, style: &Style) {\n    let mut egui_style = (*ctx.global_style()).clone();\n    set_font_sizes_style(&mut egui_style, style);\n    ctx.set_global_style(egui_style);\n}\n\npub fn set_font_sizes_style(egui_style: &mut egui::Style, style: &Style) {\n    egui_style.text_styles = [\n        (\n            Heading,\n            FontId::new(style.font_sizes.heading.into(), Proportional),\n        ),\n        (\n            Body,\n            FontId::new(style.font_sizes.body.into(), Proportional),\n        ),\n        (\n            Monospace,\n            FontId::new(style.font_sizes.monospace.into(), FontFamily::Monospace),\n        ),\n        (\n            Button,\n            FontId::new(style.font_sizes.button.into(), Proportional),\n        ),\n        (\n            Small,\n            FontId::new(style.font_sizes.small.into(), Proportional),\n        ),\n    ]\n    .into();\n}\n\nfn add_new_bookmark(app: &mut App, gui: &mut Gui, byte_off: usize) {\n    let bms = &mut app.meta_state.meta.bookmarks;\n    let idx = bms.len();\n    bms.push(Bookmark {\n        offset: byte_off,\n        label: format!(\"New @ offset {byte_off}\"),\n        desc: String::new(),\n        value_type: ValueType::U8(value_type::U8),\n    });\n    gui.win.bookmarks.open.set(true);\n    gui.win.bookmarks.selected = Some(idx);\n    gui.win.bookmarks.edit_name = true;\n    gui.win.bookmarks.focus_text_edit = true;\n}\n"
  },
  {
    "path": "src/hex_conv.rs",
    "content": "fn byte_16_digits(byte: u8) -> [u8; 2] {\n    [byte / 16, byte % 16]\n}\n\n#[test]\nfn test_byte_16_digits() {\n    assert_eq!(byte_16_digits(255), [15, 15]);\n}\n\npub fn byte_to_hex_digits(byte: u8) -> [u8; 2] {\n    const TABLE: &[u8; 16] = b\"0123456789ABCDEF\";\n\n    let [l, r] = byte_16_digits(byte);\n    [TABLE[l as usize], TABLE[r as usize]]\n}\n\n#[test]\nfn test_byte_to_hex_digits() {\n    let pairs = [\n        (255, b\"FF\"),\n        (0, b\"00\"),\n        (15, b\"0F\"),\n        (16, b\"10\"),\n        (154, b\"9A\"),\n        (167, b\"A7\"),\n        (6, b\"06\"),\n        (64, b\"40\"),\n    ];\n    for (byte, hex) in pairs {\n        assert_eq!(byte_to_hex_digits(byte), *hex);\n    }\n}\n\nfn digit_to_byte(digit: u8) -> Option<u8> {\n    Some(match digit {\n        b'0' => 0,\n        b'1' => 1,\n        b'2' => 2,\n        b'3' => 3,\n        b'4' => 4,\n        b'5' => 5,\n        b'6' => 6,\n        b'7' => 7,\n        b'8' => 8,\n        b'9' => 9,\n        b'a' | b'A' => 10,\n        b'b' | b'B' => 11,\n        b'c' | b'C' => 12,\n        b'd' | b'D' => 13,\n        b'e' | b'E' => 14,\n        b'f' | b'F' => 15,\n        _ => return None,\n    })\n}\n\npub fn merge_hex_halves(first: u8, second: u8) -> Option<u8> {\n    Some(digit_to_byte(first)? * 16 + digit_to_byte(second)?)\n}\n\n#[test]\nfn test_merge_halves() {\n    assert_eq!(merge_hex_halves(b'0', b'0'), Some(0));\n    assert_eq!(merge_hex_halves(b'0', b'f'), Some(15));\n    assert_eq!(merge_hex_halves(b'3', b'2'), Some(50));\n    assert_eq!(merge_hex_halves(b'f', b'0'), Some(240));\n    assert_eq!(merge_hex_halves(b'f', b'f'), Some(255));\n}\n"
  },
  {
    "path": "src/hex_ui.rs",
    "content": "use {\n    crate::{\n        app::interact_mode::InteractMode,\n        color::RgbaColor,\n        meta::{LayoutKey, ViewKey, region::Region},\n        timer::Timer,\n        view::ViewportRect,\n    },\n    slotmap::Key as _,\n    std::{collections::HashMap, time::Duration},\n};\n\n/// State related to the hex view ui, different from the egui gui overlay\n#[derive(Default)]\npub struct HexUi {\n    /// \"a\" point of selection. Could be smaller or larger than \"b\".\n    /// The length of selection is absolute difference between a and b\n    pub select_a: Option<usize>,\n    /// \"b\" point of selection. Could be smaller or larger than \"a\".\n    /// The length of selection is absolute difference between a and b\n    pub select_b: Option<usize>,\n    /// Extra selections on top of the a-b selection\n    pub extra_selections: Vec<Region>,\n    pub interact_mode: InteractMode = InteractMode::View,\n    pub current_layout: LayoutKey,\n    /// The currently focused view (appears with a yellow border around it)\n    #[doc(alias = \"current_view\")]\n    pub focused_view: Option<ViewKey>,\n    /// The rectangle area that's available for the hex interface\n    pub hex_iface_rect: ViewportRect,\n    pub flash_cursor_timer: Timer,\n    /// Whether to scissor views when drawing them. Useful to disable when debugging rendering.\n    pub scissor_views: bool = true,\n    /// When alt is being held, it shows things like names of views as overlays\n    pub show_alt_overlay: bool,\n    pub rulers: HashMap<ViewKey, Ruler>,\n    /// If `Some`, contains the last byte offset the cursor was clicked at, while lmb is being held down\n    pub lmb_drag_offset: Option<usize>,\n}\n\n#[derive(Default)]\npub struct Ruler {\n    pub color: RgbaColor = RgbaColor { r: 255, g: 255, b: 0,a: 255},\n    /// Horizontal offset in pixels\n    pub hoffset: i16,\n    /// Frequency of ruler lines\n    pub freq: u8 = 1,\n    /// If set, it will try to layout ruler based on the struct fields\n    pub struct_idx: Option<usize>,\n}\n\nimpl HexUi {\n    pub fn selection(&self) -> Option<Region> {\n        if let Some(a) = self.select_a\n            && let Some(b) = self.select_b\n        {\n            Some(Region {\n                begin: a.min(b),\n                end: a.max(b),\n            })\n        } else {\n            None\n        }\n    }\n    pub fn selected_regions(&self) -> impl Iterator<Item = Region> {\n        self.selection().into_iter().chain(self.extra_selections.iter().cloned())\n    }\n    pub fn clear_selections(&mut self) {\n        self.select_a = None;\n        self.select_b = None;\n        self.extra_selections.clear();\n    }\n    /// Clear existing meta references\n    pub fn clear_meta_refs(&mut self) {\n        self.current_layout = LayoutKey::null();\n        self.focused_view = None;\n    }\n\n    pub fn flash_cursor(&mut self) {\n        self.flash_cursor_timer = Timer::set(Duration::from_millis(1500));\n    }\n\n    /// If the cursor should be flashing, returns a timer value that can be used to color cursor\n    pub fn cursor_flash_timer(&self) -> Option<u32> {\n        #[expect(\n            clippy::cast_possible_truncation,\n            reason = \"\n        The duration will never be higher than u32 limit.\n\n        It doesn't make sense to set the cursor timer to extremely high values,\n        only a few seconds at most.\n        \"\n        )]\n        self.flash_cursor_timer.overtime().map(|dur| dur.as_millis() as u32)\n    }\n}\n"
  },
  {
    "path": "src/input.rs",
    "content": "use {\n    egui_sf2g::sf2g::window::{Event, Key},\n    std::collections::HashSet,\n};\n\n#[derive(Default, Debug)]\npub struct Input {\n    key_down: HashSet<Key>,\n}\n\nimpl Input {\n    pub fn update_from_event(&mut self, event: &Event) {\n        match event {\n            Event::KeyPressed { code, .. } => {\n                self.key_down.insert(*code);\n            }\n            Event::KeyReleased { code, .. } => {\n                self.key_down.remove(code);\n            }\n            _ => {}\n        }\n    }\n    pub fn key_down(&self, key: Key) -> bool {\n        self.key_down.contains(&key)\n    }\n\n    pub(crate) fn clear(&mut self) {\n        self.key_down.clear();\n    }\n}\n"
  },
  {
    "path": "src/layout.rs",
    "content": "use {\n    crate::{\n        meta::{PerspectiveMap, RegionMap, ViewKey, ViewMap, region::Region},\n        view::{ViewportRect, ViewportVec},\n    },\n    serde::{Deserialize, Serialize},\n    std::cmp::{max, min},\n};\n\n/// A view layout grid for laying out views.\n#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]\npub struct Layout {\n    pub name: String,\n    pub view_grid: Vec<Vec<ViewKey>>,\n    /// Margin around views\n    #[serde(default = \"default_margin\")]\n    pub margin: ViewportVec,\n}\n\npub const fn default_margin() -> ViewportVec {\n    ViewportVec { x: 6, y: 6 }\n}\n\nimpl Layout {\n    /// Iterate through all view keys\n    pub fn iter(&self) -> impl Iterator<Item = ViewKey> + '_ {\n        self.view_grid.iter().flatten().cloned()\n    }\n\n    pub(crate) fn idx_of_key(&self, key: ViewKey) -> Option<[usize; 2]> {\n        self.view_grid.iter().enumerate().find_map(|(row_idx, row)| {\n            let col_pos = row.iter().position(|k| *k == key)?;\n            Some([row_idx, col_pos])\n        })\n    }\n\n    pub(crate) fn view_containing_region(\n        &self,\n        reg: &Region,\n        meta: &crate::meta::Meta,\n    ) -> Option<ViewKey> {\n        self.iter()\n            .find(|view_key| meta.views[*view_key].view.contains_region(reg, meta))\n    }\n\n    pub(crate) fn contains_view(&self, key: ViewKey) -> bool {\n        self.iter().any(|k| k == key)\n    }\n\n    pub(crate) fn remove_view(&mut self, rem_key: ViewKey) {\n        self.view_grid.retain_mut(|row| {\n            row.retain(|view_key| *view_key != rem_key);\n            !row.is_empty()\n        });\n    }\n\n    pub(crate) fn remove_dangling(&mut self, map: &ViewMap) {\n        self.view_grid.retain_mut(|row| {\n            row.retain(|view_key| {\n                let mut retain = true;\n                if !map.contains_key(*view_key) {\n                    eprintln!(\n                        \"Removed dangling view {:?} from layout {}\",\n                        view_key, self.name\n                    );\n                    retain = false;\n                }\n                retain\n            });\n            !row.is_empty()\n        });\n    }\n\n    pub(crate) fn change_view_type(&mut self, current: ViewKey, new: ViewKey) {\n        if let Some(current_key) = self.view_grid.iter_mut().flatten().find(|k| **k == current) {\n            *current_key = new;\n        }\n    }\n}\n\npub fn do_auto_layout(\n    layout: &Layout,\n    view_map: &mut ViewMap,\n    hex_iface_rect: &ViewportRect,\n    perspectives: &PerspectiveMap,\n    regions: &RegionMap,\n) {\n    let layout_n_rows = i16::try_from(layout.view_grid.len()).expect(\"Too many rows in layout\");\n    let mut total_h = 0;\n    // Determine sizes\n    for row in &layout.view_grid {\n        let max_allowed_h =\n            (hex_iface_rect.h - (layout.margin.y * (layout_n_rows + 1))) / layout_n_rows;\n        let row_n_cols = i16::try_from(row.len()).expect(\"Too many columns in layout\");\n        let mut total_row_w = 0;\n        let mut max_h = 0;\n        for &view_key in row {\n            let max_allowed_w =\n                (hex_iface_rect.w - (layout.margin.x * (row_n_cols + 1))) / row_n_cols;\n            let view = &mut view_map[view_key].view;\n            let max_needed_size = view.max_needed_size(perspectives, regions);\n            let w = min(max_needed_size.x, max_allowed_w);\n            let h = min(max_needed_size.y, max_allowed_h);\n            view.viewport_rect.w = w;\n            total_row_w += w;\n            view.viewport_rect.h = h;\n            max_h = max(max_h, view.viewport_rect.h);\n        }\n        total_h += max_h;\n        // Distribute remaining width to views in order\n        let w_to_fill_viewport = hex_iface_rect.w - (layout.margin.x * (row_n_cols + 1));\n        let mut w_remaining = w_to_fill_viewport - total_row_w;\n        for &view_key in row {\n            if w_remaining <= 0 {\n                break;\n            }\n            let view = &mut view_map[view_key].view;\n            let max_needed_w = view.max_needed_size(perspectives, regions).x;\n            let missing_for_max_needed = max_needed_w - view.viewport_rect.w;\n            let can_add = min(missing_for_max_needed, w_remaining);\n            view.viewport_rect.w += can_add;\n            w_remaining -= can_add;\n        }\n    }\n    // Distribute remaining height to rows in order\n    let h_to_fill_viewport = hex_iface_rect.h - (layout.margin.y * (layout_n_rows + 1));\n    let mut h_remaining = h_to_fill_viewport - total_h;\n    for row in &layout.view_grid {\n        if h_remaining <= 0 {\n            break;\n        }\n        let mut max_can_add = 0;\n        for &view_key in row {\n            let view = &mut view_map[view_key].view;\n            let max_needed_h = view.max_needed_size(perspectives, regions).y;\n            let missing_for_max_needed = max_needed_h - view.viewport_rect.h;\n            let can_add = min(missing_for_max_needed, h_remaining);\n            max_can_add = max(max_can_add, can_add);\n            view.viewport_rect.h += can_add;\n        }\n        h_remaining -= max_can_add;\n    }\n    // Lay out\n    let mut x_cursor = hex_iface_rect.x + layout.margin.x;\n    let mut y_cursor = hex_iface_rect.y + layout.margin.y;\n    for row in &layout.view_grid {\n        let mut max_h = 0;\n        for &view_key in row {\n            let view = &mut view_map[view_key].view;\n            view.viewport_rect.x = x_cursor;\n            view.viewport_rect.y = y_cursor;\n            x_cursor += view.viewport_rect.w + layout.margin.x;\n            max_h = max(max_h, view.viewport_rect.h);\n        }\n        x_cursor = hex_iface_rect.x + layout.margin.x;\n        y_cursor += max_h + layout.margin.y;\n    }\n}\n"
  },
  {
    "path": "src/main.rs",
    "content": "#![doc(html_no_source)]\n#![feature(\n    try_blocks,\n    generic_const_exprs,\n    macro_metavar_expr_concat,\n    default_field_values,\n    yeet_expr,\n    cmp_minmax\n)]\n#![warn(\n    unused_qualifications,\n    redundant_imports,\n    trivial_casts,\n    trivial_numeric_casts,\n    unsafe_op_in_unsafe_fn,\n    clippy::unwrap_used,\n    clippy::cast_lossless,\n    clippy::cast_possible_truncation,\n    clippy::cast_precision_loss,\n    clippy::cast_sign_loss,\n    clippy::cast_possible_wrap,\n    clippy::panic,\n    clippy::needless_pass_by_ref_mut,\n    clippy::semicolon_if_nothing_returned,\n    clippy::items_after_statements,\n    clippy::unused_trait_names,\n    clippy::undocumented_unsafe_blocks,\n    clippy::uninlined_format_args,\n    clippy::format_push_string,\n    clippy::unnecessary_wraps,\n    clippy::map_unwrap_or,\n    clippy::use_self,\n    clippy::redundant_clone\n)]\n#![cfg_attr(test, allow(clippy::unwrap_used))]\n#![expect(\n    incomplete_features,\n    // It's hard to reconcile lack of partial borrows with few arguments\n    clippy::too_many_arguments\n)]\n#![windows_subsystem = \"windows\"]\n\nuse {\n    crate::app::App,\n    anyhow::Context as _,\n    args::Args,\n    clap::Parser as _,\n    config::{Config, LoadedConfig, PinnedDir, ProjectDirsExt as _},\n    constcat::concat,\n    core::f32,\n    egui_colors::{Colorix, tokens::ThemeColor},\n    egui_file_dialog::PinnedFolder,\n    egui_phosphor::regular as ic,\n    egui_sf2g::{\n        SfEgui,\n        sf2g::{\n            graphics::{Color, Font, RenderTarget as _, RenderWindow},\n            system::Vector2,\n            window::{ContextSettings, Event, Style, VideoMode},\n        },\n    },\n    gui::{Gui, command::GCmd, message_dialog::Icon},\n    mlua::Lua,\n    std::{\n        backtrace::{Backtrace, BacktraceStatus},\n        io::IsTerminal as _,\n        time::Duration,\n    },\n};\n\nmod app;\nmod args;\nmod backend;\nmod color;\nmod config;\nmod damage_region;\nmod data;\nmod dec_conv;\npub mod edit_buffer;\nmod find_util;\nmod gui;\nmod hex_conv;\nmod hex_ui;\nmod input;\nmod layout;\nmod meta;\nmod meta_state;\nmod parse_radix;\nmod plugin;\nmod result_ext;\nmod scripting;\nmod session_prefs;\nmod shell;\nmod slice_ext;\nmod source;\nmod str_ext;\nmod struct_meta_item;\nmod timer;\nmod update;\nmod util;\nmod value_color;\nmod view;\n#[cfg(windows)]\nmod windows;\n\nconst L_CONTINUE: &str = concat!(ic::WARNING, \" Continue\");\nconst L_ABORT: &str = concat!(ic::X_CIRCLE, \"Abort\");\n\nfn print_version_info() {\n    eprintln!(\n        \"Hexerator {} ({} {}), built on {}\",\n        env!(\"CARGO_PKG_VERSION\"),\n        env!(\"VERGEN_GIT_SHA\"),\n        env!(\"VERGEN_GIT_COMMIT_TIMESTAMP\"),\n        env!(\"VERGEN_BUILD_TIMESTAMP\")\n    );\n}\n\nfn try_main() -> anyhow::Result<()> {\n    // Show arg parse diagnostics in GUI window if stderr is not a terminal.\n    //\n    // This is the only way to get arg parse diagnostics on windows, due to windows_subsystem=windows\n    let mut args = if std::io::stderr().is_terminal() {\n        Args::parse()\n    } else {\n        match Args::try_parse() {\n            Ok(args) => args,\n            Err(e) => {\n                do_fatal_error_report(\n                    \"Arg parse error\",\n                    &e.to_string(),\n                    &Backtrace::force_capture(),\n                );\n                return Ok(());\n            }\n        }\n    };\n    if args.debug {\n        gamedebug_core::IMMEDIATE.set_enabled(true);\n        gamedebug_core::PERSISTENT.set_enabled(true);\n    }\n    if args.version {\n        print_version_info();\n        return Ok(());\n    }\n    let desktop_mode = VideoMode::desktop_mode();\n    let mut window = RenderWindow::new(\n        desktop_mode,\n        \"Hexerator\",\n        Style::RESIZE | Style::CLOSE,\n        &ContextSettings::default(),\n    )?;\n    let LoadedConfig {\n        config: mut cfg,\n        old_config_err,\n    } = Config::load_or_default()?;\n    window.set_vertical_sync_enabled(cfg.vsync);\n    window.set_framerate_limit(cfg.fps_limit);\n    window.set_position(Vector2::new(0, 0));\n    let mut sf_egui = SfEgui::new(&window);\n    sf_egui.context().options_mut(|opts| {\n        opts.zoom_with_keyboard = false;\n    });\n    let mut style = egui::Style::default();\n    style.interaction.show_tooltips_only_when_still = true;\n    let font = Font::from_memory_static(include_bytes!(\"../DejaVuSansMono.ttf\"))\n        .context(\"Failed to load font\")?;\n    let mut gui = Gui::default();\n    gui.win.open_process.default_meta_path.clone_from(&args.meta);\n    transfer_pinned_folders_to_file_dialog(&mut gui, &mut cfg);\n    if !args.spawn_command.is_empty() {\n        gui.cmd.push(GCmd::SpawnCommand {\n            args: std::mem::take(&mut args.spawn_command),\n            look_for_proc: args.look_for_proc.take(),\n        });\n    }\n    if let Some(e) = old_config_err {\n        gui.msg_dialog.open(\n            Icon::Error,\n            \"Failed to load old config\",\n            format!(\"Old config failed to load with error: {e}.\\n\\\n                     If you don't want to overwrite the old config, you should probably not continue.\"),\n        );\n        gui.msg_dialog.custom_button_row_ui(Box::new(|ui, payload, _cmd| {\n            if ui.button(L_CONTINUE).clicked() {\n                payload.close = true;\n            }\n            if ui.button(L_ABORT).clicked() {\n                std::process::abort();\n            }\n        }));\n    }\n    let mut font_defs = egui::FontDefinitions::default();\n    egui_phosphor::add_to_fonts(&mut font_defs, egui_phosphor::Variant::Regular);\n    egui_fontcfg::load_custom_fonts(&cfg.custom_font_paths, &mut font_defs.font_data)?;\n    // Manually ensure that there are no entries in the font config that don't have font data.\n    // Egui panics if this is the case, but we don't want to panic, so we just remove the offending\n    // font families.\n    cfg.font_families.retain(|_k, v| {\n        let mut retain = true;\n        for font_name in v {\n            if !font_defs.font_data.contains_key(font_name) {\n                eprintln!(\"Error: No font data for {font_name}. Removing.\");\n                retain = false;\n            }\n        }\n        retain\n    });\n    if !cfg.font_families.is_empty() {\n        font_defs.families = cfg.font_families.clone();\n    }\n    sf_egui.context().set_fonts(font_defs);\n    let font_size = 14;\n    #[expect(\n        clippy::cast_possible_truncation,\n        clippy::cast_sign_loss,\n        reason = \"It's extremely unlikely that the line spacing is not between 0..u16::MAX\"\n    )]\n    let line_spacing = font.line_spacing(u32::from(font_size)) as u16;\n    let mut app = App::new(args, cfg, font_size, line_spacing, &mut gui)?;\n    let lua = Lua::default();\n    gui::set_font_sizes_style(&mut style, &app.cfg.style);\n    sf_egui.context().set_global_style(style);\n    // Custom egui_colors theme load\n    if let Some(project_dirs) = config::project_dirs() {\n        let path = project_dirs.color_theme_path();\n        if path.exists() {\n            match std::fs::read(path) {\n                Ok(data) => {\n                    let mut chunks = data.as_chunks().0.iter().copied();\n                    let theme = std::array::from_fn(|_| {\n                        ThemeColor::Custom(chunks.next().unwrap_or_default())\n                    });\n                    gui.colorix = Some(Colorix::global(sf_egui.context(), theme));\n                }\n                Err(e) => {\n                    eprintln!(\"Failed to load custom theme: {e}\");\n                }\n            }\n        }\n    }\n    let mut vertex_buffer = Vec::new();\n\n    while window.is_open() {\n        if !update::do_frame(\n            &mut app,\n            &mut gui,\n            &mut sf_egui,\n            &mut window,\n            &mut vertex_buffer,\n            &lua,\n            &font,\n        )? {\n            return Ok(());\n        }\n        // Save a metafile backup every so often\n        if app.meta_state.last_meta_backup.get().elapsed() >= Duration::from_secs(60)\n            && let Err(e) = app.save_temp_metafile_backup()\n        {\n            gamedebug_core::per!(\"Failed to save temp metafile backup: {}\", e);\n        }\n    }\n    app.close_file();\n    transfer_pinned_folders_to_config(gui, &mut app);\n    app.cfg.save()?;\n    Ok(())\n}\n\nfn transfer_pinned_folders_to_file_dialog(gui: &mut Gui, cfg: &mut Config) {\n    let dia_store = gui.fileops.dialog.storage_mut();\n    // Remove them from the config, as later it will be filled with\n    // the pinned dirs from the dialog\n    for dir in cfg.pinned_dirs.drain(..) {\n        dia_store.pinned_folders.push(PinnedFolder {\n            label: dir.label,\n            path: dir.path,\n        });\n    }\n}\n\nfn transfer_pinned_folders_to_config(mut gui: Gui, app: &mut App) {\n    let storage = gui.fileops.dialog.storage_mut();\n    for entry in std::mem::take(&mut storage.pinned_folders) {\n        app.cfg.pinned_dirs.push(PinnedDir {\n            path: entry.path,\n            label: entry.label,\n        });\n    }\n}\n\nfn main() {\n    std::panic::set_hook(Box::new(|panic_info| {\n        let payload = panic_info.payload();\n        let msg = if let Some(s) = payload.downcast_ref::<&str>() {\n            s\n        } else if let Some(s) = payload.downcast_ref::<String>() {\n            s\n        } else {\n            \"Unknown panic payload\"\n        };\n        let (file, line, column) = match panic_info.location() {\n            Some(loc) => (loc.file(), loc.line().to_string(), loc.column().to_string()),\n            None => (\"unknown\", \"unknown\".into(), \"unknown\".into()),\n        };\n        let bkpath = app::temp_metafile_backup_path();\n        let bkpath = bkpath.display();\n        let btrace = Backtrace::force_capture();\n        do_fatal_error_report(\n            \"Hexerator panic\",\n            &format!(\n                \"\\\n            {msg}\\n\\n\\\n            Location:\\n\\\n            {file}:{line}:{column}\\n\\n\\\n            Meta Backup path:\\n\\\n            {bkpath}\",\n            ),\n            &btrace,\n        );\n    }));\n    if let Err(e) = try_main() {\n        do_fatal_error_report(\"Fatal error\", &e.to_string(), e.backtrace());\n    }\n}\n\nfn do_fatal_error_report(title: &str, mut desc: &str, backtrace: &Backtrace) {\n    if std::io::stderr().is_terminal() {\n        eprintln!(\"== {title} ==\");\n        eprintln!(\"{desc}\");\n        if backtrace.status() == BacktraceStatus::Captured {\n            eprintln!(\"Backtrace:\\n{backtrace}\");\n        }\n        return;\n    }\n    let bt_string = if backtrace.status() == BacktraceStatus::Captured {\n        backtrace.to_string()\n    } else {\n        String::new()\n    };\n    let mut rw =\n        match RenderWindow::new((800, 600), title, Style::CLOSE, &ContextSettings::default()) {\n            Ok(rw) => rw,\n            Err(e) => {\n                eprintln!(\"Failed to create RenderWindow: {e}\");\n                return;\n            }\n        };\n    rw.set_vertical_sync_enabled(true);\n    let mut sf_egui = SfEgui::new(&rw);\n    while rw.is_open() {\n        while let Some(ev) = rw.poll_event() {\n            sf_egui.add_event(&ev);\n            if ev == Event::Closed {\n                rw.close();\n            }\n        }\n        rw.clear(Color::BLACK);\n        #[expect(clippy::unwrap_used)]\n        let di = sf_egui\n            .run(&mut rw, |rw, ui| {\n                egui::CentralPanel::default().show_inside(ui, |ui| {\n                    ui.heading(title);\n                    ui.separator();\n                    egui::ScrollArea::vertical().auto_shrink(false).max_height(500.).show(\n                        ui,\n                        |ui| {\n                            ui.add(\n                                egui::TextEdit::multiline(&mut desc)\n                                    .code_editor()\n                                    .desired_width(f32::INFINITY),\n                            );\n                            if !bt_string.is_empty() {\n                                ui.heading(\"Backtrace\");\n                                ui.add(\n                                    egui::TextEdit::multiline(&mut bt_string.as_str())\n                                        .code_editor()\n                                        .desired_width(f32::INFINITY),\n                                );\n                            }\n                        },\n                    );\n                    ui.separator();\n                    ui.horizontal(|ui| {\n                        if ui.button(\"Copy to clipboard\").clicked() {\n                            ui.copy_text(desc.to_owned());\n                        }\n                        if ui.button(\"Close\").clicked() {\n                            rw.close();\n                        }\n                    });\n                });\n            })\n            .unwrap();\n        sf_egui.draw(di, &mut rw, None);\n        rw.display();\n    }\n}\n"
  },
  {
    "path": "src/meta/perspective.rs",
    "content": "use {\n    super::region::Region,\n    crate::meta::{RegionKey, RegionMap},\n    serde::{Deserialize, Serialize},\n};\n\n/// A \"perspectived\" (column count) view of a region\n#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]\npub struct Perspective {\n    /// The associated region\n    pub region: RegionKey,\n    /// Column count, a.k.a alignment. The proper alignment can reveal\n    /// patterns to the human eye that aren't otherwise easily recognizable.\n    pub cols: usize,\n    /// Whether row order is flipped.\n    ///\n    /// Sometimes binary files store images or other data \"upside-down\".\n    /// A row order flipped perspective helps view and manipulate this kind of data better.\n    pub flip_row_order: bool,\n    pub name: String,\n}\n\nimpl Perspective {\n    /// Returns the index of the last row\n    pub(crate) fn last_row_idx(&self, rmap: &RegionMap) -> usize {\n        rmap[self.region].region.end / self.cols\n    }\n    /// Returns the index of the last column\n    pub(crate) fn last_col_idx(&self, rmap: &RegionMap) -> usize {\n        rmap[self.region].region.end % self.cols\n    }\n    pub(crate) fn byte_offset_of_row_col(&self, row: usize, col: usize, rmap: &RegionMap) -> usize {\n        rmap[self.region].region.begin + (row * self.cols + col)\n    }\n    pub(crate) fn row_col_of_byte_offset(&self, offset: usize, rmap: &RegionMap) -> [usize; 2] {\n        let reg = &rmap[self.region];\n        let offset = offset.saturating_sub(reg.region.begin);\n        [offset / self.cols, offset % self.cols]\n    }\n    /// Whether the columns are within `cols` and the calculated offset is within the region\n    pub(crate) fn row_col_within_bound(&self, row: usize, col: usize, rmap: &RegionMap) -> bool {\n        col < self.cols\n            && rmap[self.region].region.contains(self.byte_offset_of_row_col(row, col, rmap))\n    }\n    pub(crate) fn clamp_cols(&mut self, rmap: &RegionMap) {\n        self.cols = self.cols.clamp(1, rmap[self.region].region.len());\n    }\n    /// Returns rows spanned by `region`, and the remainder\n    pub(crate) fn region_row_span(&self, region: Region) -> [usize; 2] {\n        [region.len() / self.cols, region.len() % self.cols]\n    }\n    pub(crate) fn n_rows(&self, rmap: &RegionMap) -> usize {\n        let region = &rmap[self.region].region;\n        let mut rows = region.len() / self.cols;\n        if !region.len().is_multiple_of(self.cols) {\n            rows += 1;\n        }\n        rows\n    }\n\n    pub(crate) fn from_region(key: RegionKey, name: String) -> Self {\n        Self {\n            region: key,\n            cols: 48,\n            flip_row_order: false,\n            name,\n        }\n    }\n}\n"
  },
  {
    "path": "src/meta/region.rs",
    "content": "use serde::{Deserialize, Serialize};\n\n/// An inclusive region spanning `begin` to `end`\n#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]\npub struct Region {\n    pub begin: usize,\n    pub end: usize,\n}\n\nimpl Region {\n    pub fn len(&self) -> usize {\n        // Inclusive, so add 1 to end\n        (self.end + 1).saturating_sub(self.begin)\n    }\n\n    pub(crate) fn contains(&self, idx: usize) -> bool {\n        (self.begin..=self.end).contains(&idx)\n    }\n\n    pub(crate) fn contains_region(&self, reg: &Self) -> bool {\n        self.begin <= reg.begin && self.end >= reg.end\n    }\n    pub fn to_range(self) -> std::ops::RangeInclusive<usize> {\n        self.begin..=self.end\n    }\n    /// The \"previous chunk\" with the same length\n    pub fn prev_chunk(self) -> Option<Self> {\n        let len = self.len();\n        self.begin.checked_sub(len).map(|new_begin| Self {\n            begin: new_begin,\n            // INVARIANT: begin < end\n            end: self.end - len,\n        })\n    }\n    /// The \"next chunk\" with the same length\n    pub fn next_chunk(self) -> Self {\n        let len = self.len();\n        Self {\n            begin: self.begin + len,\n            end: self.end + len,\n        }\n    }\n}\n"
  },
  {
    "path": "src/meta/value_type.rs",
    "content": "use {\n    serde::{Deserialize, Serialize},\n    std::collections::HashMap,\n};\n\n#[derive(Serialize, Deserialize, Clone, Default)]\npub enum ValueType {\n    #[default]\n    None,\n    I8(I8),\n    U8(U8),\n    I16Le(I16Le),\n    U16Le(U16Le),\n    I16Be(I16Be),\n    U16Be(U16Be),\n    I32Le(I32Le),\n    U32Le(U32Le),\n    I32Be(I32Be),\n    U32Be(U32Be),\n    I64Le(I64Le),\n    U64Le(U64Le),\n    I64Be(I64Be),\n    U64Be(U64Be),\n    F32Le(F32Le),\n    F32Be(F32Be),\n    F64Le(F64Le),\n    F64Be(F64Be),\n    StringMap(StringMap),\n}\n\nimpl PartialEq for ValueType {\n    fn eq(&self, other: &Self) -> bool {\n        core::mem::discriminant(self) == core::mem::discriminant(other)\n    }\n}\n\npub type StringMap = HashMap<u8, String>;\n\nimpl ValueType {\n    pub fn label(&self) -> &str {\n        match self {\n            Self::None => \"none\",\n            Self::I8(v) => v.label(),\n            Self::U8(v) => v.label(),\n            Self::I16Le(v) => v.label(),\n            Self::U16Le(v) => v.label(),\n            Self::I16Be(v) => v.label(),\n            Self::U16Be(v) => v.label(),\n            Self::I32Le(v) => v.label(),\n            Self::U32Le(v) => v.label(),\n            Self::I32Be(v) => v.label(),\n            Self::U32Be(v) => v.label(),\n            Self::I64Le(v) => v.label(),\n            Self::U64Le(v) => v.label(),\n            Self::I64Be(v) => v.label(),\n            Self::U64Be(v) => v.label(),\n            Self::F32Le(v) => v.label(),\n            Self::F32Be(v) => v.label(),\n            Self::F64Le(v) => v.label(),\n            Self::F64Be(v) => v.label(),\n            Self::StringMap(v) => v.label(),\n        }\n    }\n\n    pub(crate) fn byte_len(&self) -> usize {\n        match self {\n            Self::None => 1,\n            Self::I8(_) => 1,\n            Self::U8(_) => 1,\n            Self::I16Le(_) => 2,\n            Self::U16Le(_) => 2,\n            Self::I16Be(_) => 2,\n            Self::U16Be(_) => 2,\n            Self::I32Le(_) => 4,\n            Self::U32Le(_) => 4,\n            Self::I32Be(_) => 4,\n            Self::U32Be(_) => 4,\n            Self::I64Le(_) => 8,\n            Self::U64Le(_) => 8,\n            Self::I64Be(_) => 8,\n            Self::U64Be(_) => 8,\n            Self::F32Le(_) => 4,\n            Self::F32Be(_) => 4,\n            Self::F64Le(_) => 8,\n            Self::F64Be(_) => 8,\n            Self::StringMap(_) => 1,\n        }\n    }\n\n    pub fn read(&self, data: &[u8]) -> anyhow::Result<ReadValue> {\n        macro_rules! r {\n            ($t:ident $($en:ident)?) => {\n                paste::paste! {\n                    ReadValue::$t(read::<[<$t $($en)?>]>(data)?)\n                }\n            }\n        }\n        Ok(match self {\n            Self::None => r!(U8),\n            Self::I8(_) => r!(I8),\n            Self::U8(_) => r!(U8),\n            Self::I16Le(_) => r!(I16 Le),\n            Self::U16Le(_) => r!(U16 Le),\n            Self::I16Be(_) => r!(I16 Be),\n            Self::U16Be(_) => r!(U16 Be),\n            Self::I32Le(_) => r!(I32 Le),\n            Self::U32Le(_) => r!(U32 Le),\n            Self::I32Be(_) => r!(I32 Be),\n            Self::U32Be(_) => r!(U32 Be),\n            Self::I64Le(_) => r!(I64 Le),\n            Self::U64Le(_) => r!(U64 Le),\n            Self::I64Be(_) => r!(I64 Be),\n            Self::U64Be(_) => r!(U64 Be),\n            Self::F32Le(_) => r!(F32 Le),\n            Self::F32Be(_) => r!(F32 Be),\n            Self::F64Le(_) => r!(F64 Le),\n            Self::F64Be(_) => r!(F64 Be),\n            Self::StringMap(_) => r!(U8),\n        })\n    }\n}\n\nfn read<P: EndianedPrimitive>(data: &[u8]) -> Result<P::Primitive, anyhow::Error>\nwhere\n    [(); P::BYTE_LEN]:,\n{\n    Ok(P::from_bytes(data[..P::BYTE_LEN].try_into()?))\n}\n\npub enum ReadValue {\n    I8(i8),\n    U8(u8),\n    I16(i16),\n    U16(u16),\n    I32(i32),\n    U32(u32),\n    I64(i64),\n    U64(u64),\n    F32(f32),\n    F64(f64),\n}\n\nimpl std::fmt::Display for ReadValue {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::U8(v) => v.fmt(f),\n            Self::I8(v) => v.fmt(f),\n            Self::I16(v) => v.fmt(f),\n            Self::U16(v) => v.fmt(f),\n            Self::I32(v) => v.fmt(f),\n            Self::U32(v) => v.fmt(f),\n            Self::I64(v) => v.fmt(f),\n            Self::U64(v) => v.fmt(f),\n            Self::F32(v) => v.fmt(f),\n            Self::F64(v) => v.fmt(f),\n        }\n    }\n}\n\npub trait EndianedPrimitive {\n    const BYTE_LEN: usize = size_of::<Self::Primitive>();\n    type Primitive: egui::emath::Numeric + std::fmt::Display + core::str::FromStr;\n    fn from_bytes(bytes: [u8; Self::BYTE_LEN]) -> Self::Primitive;\n    fn to_bytes(prim: Self::Primitive) -> [u8; Self::BYTE_LEN];\n    fn label(&self) -> &'static str;\n    fn from_byte_slice(slice: &[u8]) -> Option<Self::Primitive>\n    where\n        [(); Self::BYTE_LEN]:,\n    {\n        match slice.try_into() {\n            Ok(slice) => Some(Self::from_bytes(slice)),\n            Err(_) => None,\n        }\n    }\n}\n\n#[derive(Serialize, Deserialize, Clone)]\npub struct I8;\n\nimpl EndianedPrimitive for I8 {\n    type Primitive = i8;\n\n    fn from_bytes(bytes: [u8; Self::BYTE_LEN]) -> Self::Primitive {\n        i8::from_ne_bytes(bytes)\n    }\n\n    fn to_bytes(prim: Self::Primitive) -> [u8; Self::BYTE_LEN] {\n        prim.to_ne_bytes()\n    }\n\n    fn label(&self) -> &'static str {\n        \"i8\"\n    }\n}\n\n#[derive(Serialize, Deserialize, Clone)]\npub struct U8;\n\nimpl EndianedPrimitive for U8 {\n    type Primitive = u8;\n\n    fn from_bytes(bytes: [u8; Self::BYTE_LEN]) -> Self::Primitive {\n        u8::from_ne_bytes(bytes)\n    }\n\n    fn to_bytes(prim: Self::Primitive) -> [u8; Self::BYTE_LEN] {\n        prim.to_ne_bytes()\n    }\n\n    fn label(&self) -> &'static str {\n        \"u8\"\n    }\n}\n\nmacro_rules! impl_for_num {\n    ($($wrap:ident => $prim:ident $en:ident,)*) => {\n        $(\n            #[derive(Serialize, Deserialize, Clone)]\n            pub struct $wrap;\n\n            impl EndianedPrimitive for $wrap {\n                type Primitive = $prim;\n\n                fn from_bytes(bytes: [u8; Self::BYTE_LEN]) -> Self::Primitive {\n                    $prim::${concat(from_, $en, _bytes)}(bytes)\n                }\n\n                fn to_bytes(prim: Self::Primitive) -> [u8; Self::BYTE_LEN] {\n                    prim.${concat(to_, $en, _bytes)}()\n                }\n\n                fn label(&self) -> &'static str {\n                    concat!(stringify!($prim), \"-\", stringify!($en))\n                }\n            }\n        )*\n    }\n}\n\nimpl_for_num! {\n    I16Le => i16 le,\n    U16Le => u16 le,\n    I16Be => i16 be,\n    U16Be => u16 be,\n    I32Le => i32 le,\n    U32Le => u32 le,\n    I32Be => i32 be,\n    U32Be => u32 be,\n    I64Le => i64 le,\n    U64Le => u64 le,\n    I64Be => i64 be,\n    U64Be => u64 be,\n    F32Le => f32 le,\n    F32Be => f32 be,\n    F64Le => f64 le,\n    F64Be => f64 be,\n}\n"
  },
  {
    "path": "src/meta.rs",
    "content": "use {\n    self::{perspective::Perspective, region::Region, value_type::ValueType},\n    crate::{layout::Layout, struct_meta_item::StructMetaItem, view::View},\n    serde::{Deserialize, Serialize},\n    slotmap::{SlotMap, new_key_type},\n    std::{collections::HashMap, io::Write as _},\n};\n\npub mod perspective;\npub mod region;\npub mod value_type;\n\nnew_key_type! {\n    pub struct PerspectiveKey;\n    pub struct RegionKey;\n    pub struct ViewKey;\n    pub struct LayoutKey;\n    pub struct ScriptKey;\n}\n\npub type PerspectiveMap = SlotMap<PerspectiveKey, Perspective>;\npub type RegionMap = SlotMap<RegionKey, NamedRegion>;\npub type ViewMap = SlotMap<ViewKey, NamedView>;\npub type LayoutMap = SlotMap<LayoutKey, Layout>;\npub type ScriptMap = SlotMap<ScriptKey, Script>;\npub type Bookmarks = Vec<Bookmark>;\n\npub trait LayoutMapExt {\n    fn add_new_default(&mut self) -> LayoutKey;\n}\n\nimpl LayoutMapExt for LayoutMap {\n    fn add_new_default(&mut self) -> LayoutKey {\n        self.insert(Layout {\n            name: \"New layout\".into(),\n            view_grid: Vec::new(),\n            margin: crate::layout::default_margin(),\n        })\n    }\n}\n\n/// A bookmark for an offset in a file\n#[derive(Serialize, Deserialize, Clone)]\npub struct Bookmark {\n    /// Offset the bookmark applies to\n    pub offset: usize,\n    /// Short label\n    pub label: String,\n    /// Extended description\n    pub desc: String,\n    /// A bookmark can optionally have a type, which can be used to display its value, etc.\n    #[serde(default)]\n    pub value_type: ValueType,\n}\nimpl Bookmark {\n    #[expect(\n        clippy::cast_possible_truncation,\n        clippy::cast_sign_loss,\n        clippy::cast_precision_loss,\n        reason = \"Not much we can do about cast errors here\"\n    )]\n    pub(crate) fn write_int(&self, mut data: &mut [u8], val: i64) -> std::io::Result<()> {\n        match self.value_type {\n            ValueType::None => Err(std::io::Error::other(\"Bookmark doesn't have value type\")),\n            ValueType::I8(_) => data.write_all(&(val as i8).to_ne_bytes()),\n            ValueType::U8(_) => data.write_all(&(val as u8).to_ne_bytes()),\n            ValueType::I16Le(_) => data.write_all(&(val as i16).to_le_bytes()),\n            ValueType::U16Le(_) => data.write_all(&(val as u16).to_le_bytes()),\n            ValueType::I16Be(_) => data.write_all(&(val as i16).to_be_bytes()),\n            ValueType::U16Be(_) => data.write_all(&(val as u16).to_be_bytes()),\n            ValueType::I32Le(_) => data.write_all(&(val as i32).to_le_bytes()),\n            ValueType::U32Le(_) => data.write_all(&(val as u32).to_le_bytes()),\n            ValueType::I32Be(_) => data.write_all(&(val as i32).to_be_bytes()),\n            ValueType::U32Be(_) => data.write_all(&(val as u32).to_be_bytes()),\n            ValueType::I64Le(_) => data.write_all(&(val).to_le_bytes()),\n            ValueType::U64Le(_) => data.write_all(&(val as u64).to_le_bytes()),\n            ValueType::I64Be(_) => data.write_all(&(val).to_be_bytes()),\n            ValueType::U64Be(_) => data.write_all(&(val as u64).to_be_bytes()),\n            ValueType::F32Le(_) => data.write_all(&(val as f32).to_le_bytes()),\n            ValueType::F32Be(_) => data.write_all(&(val as f32).to_be_bytes()),\n            ValueType::F64Le(_) => data.write_all(&(val as f64).to_le_bytes()),\n            ValueType::F64Be(_) => data.write_all(&(val as f64).to_be_bytes()),\n            ValueType::StringMap(_) => data.write_all(&(val as u8).to_ne_bytes()),\n        }\n    }\n}\n\n/// \"Low\" region of the meta, containing the least dependent data, like regions and perspectives\n#[derive(Default, Serialize, Deserialize, Clone)]\npub struct MetaLow {\n    pub regions: RegionMap,\n    pub perspectives: PerspectiveMap,\n}\n\nimpl MetaLow {\n    pub(crate) fn start_offset_of_view(&self, view: &View) -> usize {\n        let p = &self.perspectives[view.perspective];\n        self.regions[p.region].region.begin\n    }\n\n    pub(crate) fn end_offset_of_view(&self, view: &View) -> usize {\n        let p = &self.perspectives[view.perspective];\n        self.regions[p.region].region.end\n    }\n}\n\n/// Meta-information about a file that the user collects.\n#[derive(Default, Serialize, Deserialize, Clone)]\npub struct Meta {\n    pub low: MetaLow,\n    pub views: ViewMap,\n    pub layouts: LayoutMap,\n    pub bookmarks: Bookmarks,\n    pub misc: Misc,\n    #[serde(default)]\n    pub vars: HashMap<String, VarEntry>,\n    #[serde(default)]\n    pub scripts: ScriptMap,\n    /// Script to execute when a document loads\n    #[serde(default)]\n    pub onload_script: Option<ScriptKey>,\n    #[serde(default)]\n    pub structs: Vec<StructMetaItem>,\n}\n\n#[derive(Serialize, Deserialize, Clone)]\npub struct VarEntry {\n    pub val: VarVal,\n    pub desc: String,\n}\n\n#[derive(Serialize, Deserialize, Clone, PartialEq)]\npub enum VarVal {\n    I64(i64),\n    U64(u64),\n}\n\npub(crate) fn find_most_specific_region_for_offset(\n    regions: &RegionMap,\n    off: usize,\n) -> Option<RegionKey> {\n    let mut most_specific = None;\n    for (key, reg) in regions.iter() {\n        if reg.region.contains(off) {\n            match &mut most_specific {\n                Some(most_spec_key) => {\n                    // A region is more specific if it's smaller\n                    let most_spec_reg = &regions[*most_spec_key];\n                    if reg.region.len() < most_spec_reg.region.len() {\n                        *most_spec_key = key;\n                    }\n                }\n                None => most_specific = Some(key),\n            }\n        }\n    }\n    most_specific\n}\n\n/// Misc information that's worth saving\n#[derive(Serialize, Deserialize, Clone)]\npub struct Misc {\n    /// Lua script for the \"Lua fill\" feature.\n    ///\n    /// Worth saving because it can be used for binary file change testing, which can\n    /// take a long time over many sessions.\n    pub fill_lua_script: String,\n    /// Lua script for the \"execute script\" feature.\n    pub exec_lua_script: String,\n}\n\nimpl Default for Misc {\n    fn default() -> Self {\n        Self {\n            fill_lua_script: DEFAULT_FILL.into(),\n            exec_lua_script: String::new(),\n        }\n    }\n}\n\nconst DEFAULT_FILL: &str = include_str!(concat!(env!(\"CARGO_MANIFEST_DIR\"), \"/lua/fill.lua\"));\n\nimpl Meta {\n    /// Init required after deserializing\n    pub fn post_load_init(&mut self) {\n        for view in self.views.values_mut() {\n            // Needed to initialize edit buffers, etc.\n            view.view.adjust_state_to_kind();\n        }\n    }\n    /// Returns offset and reference to a bookmark, if it corresponds to an offset\n    pub fn bookmark_for_offset(\n        meta_bookmarks: &Bookmarks,\n        off: usize,\n    ) -> Option<(usize, &Bookmark)> {\n        meta_bookmarks.iter().enumerate().find(|(_i, b)| b.offset == off)\n    }\n\n    pub(crate) fn add_region_from_selection(&mut self, sel: Region) -> RegionKey {\n        self.low.regions.insert(NamedRegion::new_from_selection(sel))\n    }\n\n    pub(crate) fn remove_view(&mut self, rem_key: ViewKey) {\n        self.views.remove(rem_key);\n\n        for layout in self.layouts.values_mut() {\n            layout.remove_view(rem_key);\n        }\n    }\n\n    pub(crate) fn bookmark_by_name_mut(&mut self, name: &str) -> Option<&mut Bookmark> {\n        self.bookmarks.iter_mut().find(|bm| bm.label == name)\n    }\n\n    pub(crate) fn region_by_name_mut(&mut self, name: &str) -> Option<&mut NamedRegion> {\n        self.low.regions.iter_mut().find_map(|(_k, v)| (v.name == name).then_some(v))\n    }\n    /// Remove anything that contains dangling keys\n    pub(crate) fn remove_dangling(&mut self) {\n        self.low.perspectives.retain(|_k, v| {\n            let mut retain = true;\n            if !self.low.regions.contains_key(v.region) {\n                eprintln!(\"Removed dangling perspective '{}'\", v.name);\n                retain = false;\n            }\n            retain\n        });\n        self.views.retain(|_k, v| {\n            let mut retain = true;\n            if !self.low.perspectives.contains_key(v.view.perspective) {\n                eprintln!(\"Removed dangling view '{}'\", v.name);\n                retain = false;\n            }\n            retain\n        });\n        for layout in self.layouts.values_mut() {\n            layout.remove_dangling(&self.views);\n        }\n    }\n}\n\n#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]\npub struct NamedRegion {\n    pub name: String,\n    pub region: Region,\n    #[serde(default)]\n    pub desc: String,\n}\n\n#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]\npub struct NamedView {\n    pub name: String,\n    pub view: View,\n}\n\nimpl NamedRegion {\n    pub fn new(name: String, begin: usize, end: usize) -> Self {\n        Self {\n            name,\n            region: Region { begin, end },\n            desc: String::new(),\n        }\n    }\n    pub fn new_from_selection(sel: Region) -> Self {\n        Self {\n            name: format!(\"New ({}..={})\", sel.begin, sel.end),\n            region: sel,\n            desc: String::new(),\n        }\n    }\n}\n\n#[derive(Serialize, Deserialize, Clone)]\npub struct Script {\n    pub name: String,\n    pub desc: String,\n    pub content: String,\n}\n"
  },
  {
    "path": "src/meta_state.rs",
    "content": "use {\n    crate::meta::Meta,\n    std::{cell::Cell, path::PathBuf, time::Instant},\n};\n\npub struct MetaState {\n    pub last_meta_backup: Cell<Instant>,\n    pub current_meta_path: PathBuf,\n    /// Clean copy of the metadata from last load/save\n    pub clean_meta: Meta,\n    pub meta: Meta,\n}\n\nimpl Default for MetaState {\n    fn default() -> Self {\n        Self {\n            meta: Meta::default(),\n            clean_meta: Meta::default(),\n            last_meta_backup: Cell::new(Instant::now()),\n            current_meta_path: PathBuf::new(),\n        }\n    }\n}\n"
  },
  {
    "path": "src/parse_radix.rs",
    "content": "use num_traits::Num;\n\npub fn parse_guess_radix<T: Num>(input: &str) -> Result<T, <T as Num>::FromStrRadixErr> {\n    if let Some(stripped) = input.strip_prefix(\"0x\") {\n        T::from_str_radix(stripped, 16)\n    } else if input.contains(['a', 'b', 'c', 'd', 'e', 'f']) {\n        T::from_str_radix(input, 16)\n    } else {\n        T::from_str_radix(input, 10)\n    }\n}\n\n/// Relativity of an offset\npub enum Relativity {\n    Absolute,\n    RelAdd,\n    RelSub,\n}\n\npub fn parse_offset_maybe_relative(\n    input: &str,\n) -> Result<(usize, Relativity), <usize as Num>::FromStrRadixErr> {\n    Ok(if let Some(stripped) = input.strip_prefix('-') {\n        (parse_guess_radix(stripped.trim_end())?, Relativity::RelSub)\n    } else if let Some(stripped) = input.strip_prefix('+') {\n        (parse_guess_radix(stripped.trim_end())?, Relativity::RelAdd)\n    } else {\n        (parse_guess_radix(input.trim_end())?, Relativity::Absolute)\n    })\n}\n"
  },
  {
    "path": "src/plugin.rs",
    "content": "use {\n    crate::{app::App, meta::PerspectiveKey},\n    hexerator_plugin_api::{HexeratorHandle, PerspectiveHandle, Plugin, PluginMethod},\n    slotmap::{Key as _, KeyData},\n    std::path::PathBuf,\n};\n\npub struct PluginContainer {\n    pub path: PathBuf,\n    pub plugin: Box<dyn Plugin>,\n    pub methods: Vec<PluginMethod>,\n    // Safety: Must be last, fields are dropped in decl order.\n    pub _lib: libloading::Library,\n}\n\nimpl HexeratorHandle for App {\n    fn debug_log(&self, msg: &str) {\n        gamedebug_core::per!(\"{msg}\");\n    }\n\n    fn get_data(&self, start: usize, end: usize) -> Option<&[u8]> {\n        self.data.get(start..=end)\n    }\n\n    fn get_data_mut(&mut self, start: usize, end: usize) -> Option<&mut [u8]> {\n        self.data.get_mut(start..=end)\n    }\n\n    fn selection_range(&self) -> Option<[usize; 2]> {\n        self.hex_ui.selection().map(|sel| [sel.begin, sel.end])\n    }\n\n    fn perspective(&self, name: &str) -> Option<PerspectiveHandle> {\n        let key = self\n            .meta_state\n            .meta\n            .low\n            .perspectives\n            .iter()\n            .find_map(|(k, per)| (per.name == name).then_some(k))?;\n        Some(PerspectiveHandle {\n            key_data: key.data().as_ffi(),\n        })\n    }\n\n    fn perspective_rows(&self, ph: &PerspectiveHandle) -> Vec<&[u8]> {\n        let key: PerspectiveKey = KeyData::from_ffi(ph.key_data).into();\n        let per = &self.meta_state.meta.low.perspectives[key];\n        let regs = &self.meta_state.meta.low.regions;\n        let mut out = Vec::new();\n        let n_rows = per.n_rows(regs);\n        for row_idx in 0..n_rows {\n            let begin = per.byte_offset_of_row_col(row_idx, 0, regs);\n            out.push(&self.data[begin..begin + per.cols]);\n        }\n        out\n    }\n}\n\nimpl PluginContainer {\n    pub unsafe fn new(path: PathBuf) -> anyhow::Result<Self> {\n        // Safety: This will cause UB on a bad plugin. Nothing we can do.\n        //\n        // It's up to the user not to load bad plugins.\n        unsafe {\n            let lib = libloading::Library::new(&path)?;\n            let plugin_init = lib.get::<fn() -> Box<dyn Plugin>>(b\"hexerator_plugin_new\")?;\n            let plugin = plugin_init();\n            Ok(Self {\n                path,\n                methods: plugin.methods(),\n                plugin,\n                _lib: lib,\n            })\n        }\n    }\n}\n"
  },
  {
    "path": "src/result_ext.rs",
    "content": "pub trait AnyhowConv<T, E>\nwhere\n    anyhow::Error: From<E>,\n{\n    fn how(self) -> anyhow::Result<T>;\n}\n\nimpl<T, E> AnyhowConv<T, E> for Result<T, E>\nwhere\n    anyhow::Error: From<E>,\n{\n    fn how(self) -> anyhow::Result<T> {\n        self.map_err(anyhow::Error::from)\n    }\n}\n"
  },
  {
    "path": "src/scripting.rs",
    "content": "use {\n    crate::{\n        app::App,\n        gui::{ConMsg, Gui},\n        meta::{\n            Bookmark, NamedRegion, ScriptKey,\n            region::Region,\n            value_type::{self, EndianedPrimitive as _, ValueType},\n        },\n        slice_ext::SliceExt as _,\n    },\n    anyhow::Context as _,\n    mlua::{ExternalError as _, ExternalResult as _, IntoLuaMulti, Lua, UserData},\n    std::collections::HashMap,\n};\n\npub struct LuaExecContext<'app, 'gui> {\n    pub app: &'app mut App,\n    pub gui: &'gui mut Gui,\n    pub key: Option<ScriptKey>,\n    pub font_size: u16,\n    pub line_spacing: u16,\n}\n\npub(crate) trait Method {\n    /// Name of the method\n    const NAME: &'static str;\n    /// Help text for the method\n    const HELP: &'static str;\n    /// Stringified API signature for help purposes\n    const API_SIG: &'static str;\n    /// Arguments the method takes when called\n    type Args;\n    /// Return type\n    type Ret: IntoLuaMulti;\n    /// The function that gets called\n    fn call(lua: &Lua, exec: &mut LuaExecContext, args: Self::Args) -> mlua::Result<Self::Ret>;\n}\n\nmacro_rules! def_method {\n    ($help:literal $name:ident($lua:ident, $exec:ident, $($argname:ident: $argty:ty),*) -> $ret:ty $block:block) => {\n        #[allow(non_camel_case_types)] pub(crate) enum $name {}\n        impl Method for $name {\n            const NAME: &'static str = stringify!($name);\n            const HELP: &'static str = $help;\n            const API_SIG: &'static str = concat!(stringify!($name), \"(\", $(stringify!($argname), \": \", stringify!($argty), \", \",)* \")\", \" -> \", stringify!($ret));\n            type Args = ($($argty,)*);\n            type Ret = $ret;\n            fn call($lua: &Lua, $exec: &mut LuaExecContext, ($($argname,)*): ($($argty,)*)) -> mlua::Result<$ret> $block\n        }\n    };\n}\n\ndef_method! {\n    \"Adds a region to the meta\"\n    add_region(_lua, exec, name: String, begin: usize, end: usize) -> () {\n        exec.app.meta_state.meta.low.regions.insert(NamedRegion {\n            name,\n            desc: String::new(),\n            region: Region { begin, end },\n        });\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Loads a file\"\n    load_file(_lua, exec, path: String) -> () {\n        exec.app\n            .load_file(path.into(), true, &mut exec.gui.msg_dialog, exec.font_size, exec.line_spacing);\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Sets the value pointed to by the bookmark to an integer value\"\n    bookmark_set_int(_lua, exec, name: String, val: i64) -> () {\n        let bm = exec\n            .app\n            .meta_state\n            .meta\n            .bookmark_by_name_mut(&name)\n            .ok_or(\"no such bookmark\".into_lua_err())?;\n        bm.write_int(&mut exec.app.data[bm.offset..], val).map_err(|e| e.into_lua_err())?;\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Fills a named region with a pattern\"\n    region_pattern_fill(_lua, exec, name: String, pattern: String) -> () {\n        let reg = exec\n            .app\n            .meta_state\n            .meta\n            .region_by_name_mut(&name)\n            .ok_or(\"no such region\".into_lua_err())?;\n        let pat = crate::find_util::parse_hex_string(&pattern).map_err(|e| e.into_lua_err())?;\n        exec.app.data[reg.region.begin..=reg.region.end].pattern_fill(&pat);\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Returns an array containing the offsets of the find results\"\n    find_result_offsets(_lua, exec,) -> Vec<usize> {\n        Ok(exec.gui.win.find.results_vec.clone())\n    }\n}\n\ndef_method! {\n    \"Reads an unsigned 8 bit integer at `offset`\"\n    read_u8(_lua, exec, offset: usize) -> u8 {\n        match exec.app.data.get(offset) {\n            Some(byte) => Ok(*byte),\n            None => Err(\"out of bounds\".into_lua_err()),\n        }\n    }\n}\n\ndef_method! {\n    \"Sets unsigned 8 bit integer at `offset` to `value`\"\n    write_u8(_lua, exec, offset: usize, value: u8) -> () {\n        match exec.app.data.get_mut(offset) {\n            Some(byte) => {\n                *byte = value;\n                Ok(())\n            }\n            None => Err(\"out of bounds\".into_lua_err())\n        }\n    }\n}\n\ndef_method! {\n    \"Reads a little endian unsigned 16 bit integer at `offset`\"\n    read_u16_le(_lua, exec, offset: usize) -> u16 {\n        match exec\n        .app\n        .data\n        .get(offset..offset + 2)\n    {\n        Some(slice) => value_type::U16Le::from_byte_slice(slice)\n            .ok_or_else(|| \"Failed to convert\".into_lua_err()),\n        None => Err(\"out of bounds\".into_lua_err()),\n    }\n    }\n}\n\ndef_method! {\n    \"Reads a little endian unsigned 32 bit integer at `offset`\"\n    read_u32_le(_lua, exec, offset: usize) -> u32 {\n        match exec\n        .app\n        .data\n        .get(offset..offset + 4)\n    {\n        Some(slice) => value_type::U32Le::from_byte_slice(slice)\n            .ok_or_else(|| \"Failed to convert\".into_lua_err()),\n        None => Err(\"out of bounds\".into_lua_err()),\n    }\n    }\n}\n\ndef_method! {\n    \"Reads a binary blob at `offset` of length `len`\"\n    read_blob(_lua, exec, offset: usize, len: usize) -> Vec<u8> {\n        match exec\n        .app\n        .data\n        .get(offset..offset + len)\n    {\n        Some(slice) => Ok(slice.to_vec()),\n        None => Err(\"out of bounds\".into_lua_err()),\n    }\n    }\n}\n\ndef_method! {\n    \"Saves binary blob `blob` to `path` on the filesystem\"\n    save_blob(_lua, _exec, blob: Vec<u8>, path: String) -> () {\n        std::fs::write(path, blob).into_lua_err()\n    }\n}\n\ndef_method! {\n    \"Fills a range from `start` to `end` with the value `fill`\"\n    fill_range(_lua, exec, start: usize, end: usize, fill: u8) -> () {\n        match exec\n              .app\n              .data\n              .get_mut(start..end) {\n            Some(slice) => {\n                slice.fill(fill);\n                Ok(())\n            }\n            None => Err(\"out of bounds\".into_lua_err()),\n        }\n    }\n}\n\ndef_method! {\n    \"Sets the dirty region to `begin..=end`\"\n    set_dirty_region(_lua, exec, begin: usize, end: usize) -> () {\n        exec.app.data.dirty_region = Some(Region { begin, end });\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Save the currently opened document (its dirty ranges)\"\n    save(_lua, exec,) -> () {\n        exec.app.save(&mut exec.gui.msg_dialog).into_lua_err()?;\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Returns the offset pointed to by the bookmark `name`\"\n    bookmark_offset(_lua, exec, name: String) -> usize {\n        match exec\n             .app\n             .meta_state\n             .meta\n             .bookmark_by_name_mut(&name)\n        {\n            Some(bm) => Ok(bm.offset),\n            None => Err(format!(\"no such bookmark: {name}\").into_lua_err()),\n        }\n    }\n}\n\ndef_method! {\n    \"Returns the `beginning`, `end` offsets of region `name`\"\n    region(_lua, exec, name: String) -> (usize, usize) {\n        match exec\n             .app\n             .meta_state\n             .meta\n             .region_by_name_mut(&name)\n        {\n            Some(reg) => Ok((reg.region.begin, reg.region.end)),\n            None => Err(format!(\"no such region: {name}\").into_lua_err()),\n        }\n    }\n}\n\ndef_method! {\n    \"Clears all bookmarks\"\n    clear_bookmarks(_lua, exec,) -> () {\n        exec.app.meta_state.meta.bookmarks.clear();\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Adds a bookmark with name `name`, pointing at `offset`\"\n    add_bookmark(_lua, exec, offset: usize, name: String) -> () {\n        exec.app.meta_state.meta.bookmarks.push(Bookmark {\n            offset,\n            label: name,\n            desc: String::new(),\n            value_type: ValueType::None,\n        });\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Finds a hex string in the format '99 aa bb ...' format, and returns its offset\"\n    find_hex_string(_lua, exec, hex_string: String) -> Option<usize> {\n        let mut offset = None;\n        crate::find_util::find_hex_string(&hex_string, &exec.app.data, |off| {\n            offset = Some(off);\n        }).into_lua_err()?;\n        Ok(offset)\n    }\n}\n\ndef_method! {\n    \"Set the cursor to `offset`, center the view on the cursor, and flash the cursor\"\n    focus_cursor(_lua, exec, offset: usize) -> () {\n        exec.app.search_focus(offset);\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Reoffsets all bookmarks based on the difference between a bookmark's and the cursor's offsets\"\n    reoffset_bookmarks_cursor_diff(_lua, exec, bookmark_name: String) -> () {\n        let bookmark = exec.app.meta_state.meta.bookmark_by_name_mut(&bookmark_name).context(\"No such bookmark\").into_lua_err()?;\n        let offset = bookmark.offset;\n        exec.app.reoffset_bookmarks_cursor_diff(offset);\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Prints to the lua console\"\n    log(_lua, exec, value: String) -> () {\n        exec.gui.win.lua_console.open.set(true);\n        exec.gui.win.lua_console.active_msg_buf = exec.key;\n        exec.gui.win.lua_console.msg_buf_for_key(exec.key).push(ConMsg::Plain(value));\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Prints a clickable offset link to the lua console with an optional text\"\n    loffset(_lua, exec, offset: usize, text: Option<String>) -> () {\n        exec.gui.win.lua_console.open.set(true);\n        exec.gui.win.lua_console.active_msg_buf = exec.key;\n        exec.gui.win.lua_console.msg_buf_for_key(exec.key).push(ConMsg::OffsetLink { text: text.map_or(offset.to_string(), |text| format!(\"{offset}: {text}\")), offset });\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Prints a clickable (inclusive) range link to the lua console with an optional text\"\n    lrange(_lua, exec, start: usize, end: usize, text: Option<String>) -> () {\n        exec.gui.win.lua_console.open.set(true);\n        exec.gui.win.lua_console.active_msg_buf = exec.key;\n        let fmt = move || { format!(\"{start}..={end}\")};\n        exec.gui.win.lua_console.msg_buf_for_key(exec.key).push(ConMsg::RangeLink { text: text.map_or_else(fmt, |text| format!(\"{}: {text}\", fmt())), start, end });\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Returns the start and end offsets of the selection\"\n    selection(_lua, exec,) -> (usize, usize) {\n        exec.app.hex_ui.selection().map(|reg| (reg.begin, reg.end)).context(\"Selection is empty\").into_lua_err()\n    }\n}\n\ndef_method! {\n    \"Gets a named script as a callable function. `hx:require('myscript')()`\"\n    require(lua, exec, name: String) -> mlua::Function {\n        let s = exec.app.meta_state.meta.scripts.values().find(|scr| scr.name == name).ok_or_else(|| \"no such script\".into_lua_err())?;\n        let chunk = lua.load(&s.content);\n        chunk.into_function()\n    }\n}\n\ndef_method! {\n    \"Executes another script with the provided (optional) arguments\"\n    exec(lua, exec, name: String, args: Option<String>) -> () {\n        let args = args.as_deref().unwrap_or(\"\");\n        if let Some((key, scr)) = exec.app.meta_state.meta.scripts.iter().find(|(_key, scr)| scr.name == name) {\n            let script = scr.content.clone();\n            exec_lua(lua, &script, exec.app, exec.gui,  args, Some(key), exec.font_size, exec.line_spacing).into_lua_err()?;\n        }\n        Ok(())\n    }\n}\n\ndef_method! {\n    \"Calls a plugin method\"\n    call_plugin(lua, exec, plugin_name: String, method_name: String, args: mlua::Variadic<mlua::Value>) -> mlua::Value {\n        let method_args: Vec<_> = args.into_iter().map(lua_plugin_value_conv).collect();\n        let val = exec.app.call_plugin_method(&plugin_name, &method_name, &method_args).into_lua_err()?;\n        match val {\n            None => Ok(mlua::Value::Nil),\n            Some(val) => Ok(plugin_value_lua_conv(val, lua)?),\n        }\n    }\n}\n\n#[expect(clippy::cast_sign_loss)]\nfn lua_plugin_value_conv(lval: mlua::Value) -> Option<hexerator_plugin_api::Value> {\n    match lval {\n        mlua::Value::Nil => None,\n        mlua::Value::Boolean(_) => todo!(),\n        mlua::Value::LightUserData(_) => todo!(),\n        mlua::Value::Integer(num) => Some(hexerator_plugin_api::Value::U64(num as u64)),\n        mlua::Value::Number(num) => Some(hexerator_plugin_api::Value::F64(num)),\n        mlua::Value::String(_) => todo!(),\n        mlua::Value::Table(_) => todo!(),\n        mlua::Value::Function(_) => todo!(),\n        mlua::Value::Thread(_) => todo!(),\n        mlua::Value::UserData(_) => todo!(),\n        mlua::Value::Error(_) => todo!(),\n        _ => todo!(),\n    }\n}\n\n#[expect(clippy::cast_precision_loss)]\nfn plugin_value_lua_conv(\n    pval: hexerator_plugin_api::Value,\n    lua: &Lua,\n) -> mlua::Result<mlua::Value> {\n    match pval {\n        hexerator_plugin_api::Value::U64(num) => Ok(mlua::Value::Number(num as f64)),\n        hexerator_plugin_api::Value::F64(num) => Ok(mlua::Value::Number(num)),\n        hexerator_plugin_api::Value::String(s) => Ok(mlua::Value::String(lua.create_string(s)?)),\n    }\n}\n\nmacro_rules! for_each_method {\n    ($m:ident) => {\n        $m!(add_region);\n        $m!(load_file);\n        $m!(bookmark_set_int);\n        $m!(region_pattern_fill);\n        $m!(find_result_offsets);\n        $m!(read_u8);\n        $m!(write_u8);\n        $m!(read_u16_le);\n        $m!(read_u32_le);\n        $m!(read_blob);\n        $m!(save_blob);\n        $m!(fill_range);\n        $m!(set_dirty_region);\n        $m!(save);\n        $m!(bookmark_offset);\n        $m!(region);\n        $m!(clear_bookmarks);\n        $m!(add_bookmark);\n        $m!(find_hex_string);\n        $m!(focus_cursor);\n        $m!(reoffset_bookmarks_cursor_diff);\n        $m!(log);\n        $m!(loffset);\n        $m!(lrange);\n        $m!(selection);\n        $m!(require);\n        $m!(exec);\n        $m!(call_plugin);\n    };\n}\npub(super) use for_each_method;\n\nimpl UserData for LuaExecContext<'_, '_> {\n    fn add_methods<T: mlua::UserDataMethods<Self>>(methods: &mut T) {\n        macro_rules! add_method {\n            ($t:ty) => {\n                methods.add_method_mut(<$t>::NAME, <$t>::call)\n            };\n        }\n        for_each_method!(add_method);\n    }\n}\n\n#[derive(thiserror::Error, Debug)]\npub enum ExecLuaError {\n    #[error(\"Failed to parse arguments: {0}\")]\n    ArgParse(#[from] ArgParseError),\n    #[error(\"Failed to execute lua: {0}\")]\n    Lua(#[from] mlua::prelude::LuaError),\n}\n\npub fn exec_lua(\n    lua: &Lua,\n    lua_script: &str,\n    app: &mut App,\n    gui: &mut Gui,\n    args: &str,\n    key: Option<ScriptKey>,\n    font_size: u16,\n    line_spacing: u16,\n) -> Result<Option<String>, ExecLuaError> {\n    let args_table = lua.create_table()?;\n    if !args.is_empty() {\n        let args = parse_script_args(args)?;\n        for (k, v) in args.into_iter() {\n            match v {\n                ScriptArg::String(s) => args_table.set(k, s)?,\n                ScriptArg::Num(n) => args_table.set(k, n)?,\n            }\n        }\n    }\n    let mut out = None;\n    lua.scope(|scope| {\n        let chunk = lua.load(lua_script);\n        let fun = chunk.into_function()?;\n        let app = scope.create_userdata(LuaExecContext {\n            app: &mut *app,\n            gui,\n            key,\n            font_size,\n            line_spacing,\n        })?;\n        if let Some(env) = fun.environment() {\n            env.set(\"hx\", app)?;\n            env.set(\"args\", args_table)?;\n        }\n        out = fun.call(())?;\n        Ok(())\n    })?;\n    Ok(out)\n}\n\n#[derive(Debug, PartialEq)]\npub enum ScriptArg {\n    String(String),\n    Num(f64),\n}\n\npub const SCRIPT_ARG_FMT_HELP_STR: &str = \"mynum = 4.5, mystring = \\\"hello\\\"\";\n\n#[derive(thiserror::Error, Debug)]\npub enum ArgParseError {\n    #[error(\"Argument must be of format 'a=b'\")]\n    ArgNotAEqB,\n    #[error(\"Unterminated string literal\")]\n    UnterminatedString,\n    #[error(\"Error parsing number: {0}\")]\n    NumParse(#[from] std::num::ParseFloatError),\n    #[error(\"Missing value after assignment\")]\n    MissingValue,\n}\n\n/// Parse script arguments\npub fn parse_script_args(s: &str) -> Result<HashMap<String, ScriptArg>, ArgParseError> {\n    let mut hm = HashMap::new();\n    let assignments = s.split(',');\n    for assignment in assignments {\n        match assignment.split_once('=') {\n            Some((lhs, rhs)) => {\n                let key = lhs.trim();\n                let strval = rhs.trim();\n                let Some(first_byte) = strval.bytes().next() else {\n                    return Err(ArgParseError::MissingValue);\n                };\n                if let Some(strval) = strval.strip_prefix(['\"', '\\'']) {\n                    let Some(end) = strval.find(first_byte as char) else {\n                        return Err(ArgParseError::UnterminatedString);\n                    };\n                    hm.insert(\n                        key.to_string(),\n                        ScriptArg::String(strval[..end].to_string()),\n                    );\n                } else {\n                    let num: f64 = strval.parse()?;\n                    hm.insert(key.to_string(), ScriptArg::Num(num));\n                }\n            }\n            None => {\n                return Err(ArgParseError::ArgNotAEqB);\n            }\n        }\n    }\n    Ok(hm)\n}\n\n#[test]\n#[expect(clippy::unwrap_used)]\nfn test_parse_script_args() {\n    let args = parse_script_args(SCRIPT_ARG_FMT_HELP_STR).unwrap();\n    assert_eq!(args.get(\"mynum\"), Some(&ScriptArg::Num(4.5)));\n    assert_eq!(\n        args.get(\"mystring\"),\n        Some(&ScriptArg::String(\"hello\".to_string()))\n    );\n}\n\n#[test]\n#[expect(clippy::unwrap_used)]\nfn test_parse_script_args_single_quot() {\n    let args = parse_script_args(\" myval = 'hello world' \").unwrap();\n    assert_eq!(\n        args.get(\"myval\"),\n        Some(&ScriptArg::String(\"hello world\".to_string()))\n    );\n}\n"
  },
  {
    "path": "src/session_prefs.rs",
    "content": "/// Preferences that only last during the current session, they are not saved\n#[derive(Debug, Default)]\npub struct SessionPrefs {\n    /// Move the edit cursor with the cursor keys, instead of block cursor\n    pub move_edit_cursor: bool,\n    /// Immediately apply changes when editing a value, instead of having\n    /// to type everything or press enter\n    pub quick_edit: bool,\n    /// Don't move the cursor after editing is finished\n    pub sticky_edit: bool,\n    /// Automatically save when editing is finished\n    pub auto_save: bool,\n    /// Keep metadata when loading.\n    pub keep_meta: bool,\n    /// Try to stay on current column when changing column count\n    pub col_change_lock_col: bool,\n    /// Try to stay on current row when changing column count\n    pub col_change_lock_row: bool = true,\n    /// Background color (mostly for fun)\n    pub bg_color: [f32; 3] = [0.0; 3],\n    /// If true, auto-reload the current file at specified interval\n    pub auto_reload: Autoreload = Autoreload::Disabled,\n    /// Auto-reload interval in milliseconds\n    pub auto_reload_interval_ms: u32 = 250,\n    /// Hide the edit cursor\n    pub hide_cursor: bool,\n}\n\n/// Autoreload behavior\n#[derive(Debug, PartialEq)]\npub enum Autoreload {\n    /// No autoreload\n    Disabled,\n    /// Autoreload all data\n    All,\n    /// Only autoreload the data visible in the active layout\n    Visible,\n}\n\nimpl Autoreload {\n    /// Whether any autoreload is active\n    pub fn is_active(&self) -> bool {\n        !matches!(self, Self::Disabled)\n    }\n    pub fn label(&self) -> &'static str {\n        match self {\n            Self::Disabled => \"disabled\",\n            Self::All => \"all\",\n            Self::Visible => \"visible only\",\n        }\n    }\n}\n"
  },
  {
    "path": "src/shell.rs",
    "content": "use {\n    crate::{\n        app::App,\n        gui::message_dialog::{Icon, MessageDialog},\n    },\n    std::backtrace::Backtrace,\n};\n\npub fn open_previous(app: &App, load: &mut Option<crate::args::SourceArgs>) {\n    if let Some(src_args) = app.cfg.recent.iter().nth(1) {\n        *load = Some(src_args.clone());\n    }\n}\n\npub fn msg_if_fail<T, E: std::fmt::Display>(\n    result: Result<T, E>,\n    prefix: &str,\n    msg: &mut MessageDialog,\n) -> Option<E> {\n    if let Err(e) = result {\n        msg_fail(&e, prefix, msg);\n        Some(e)\n    } else {\n        None\n    }\n}\n\npub fn msg_fail<E: std::fmt::Display>(e: &E, prefix: &str, msg: &mut MessageDialog) {\n    msg.open(Icon::Error, \"Error\", format!(\"{prefix}: {e:#}\"));\n    msg.set_backtrace_for_top(Backtrace::force_capture());\n}\n"
  },
  {
    "path": "src/slice_ext.rs",
    "content": "pub trait SliceExt {\n    fn pattern_fill(&mut self, pattern: &Self);\n}\n\nimpl<T: Copy> SliceExt for [T] {\n    fn pattern_fill(&mut self, pattern: &Self) {\n        for (src, dst) in pattern.iter().cycle().zip(self.iter_mut()) {\n            *dst = *src;\n        }\n    }\n}\n\n#[test]\nfn test_pattern_fill() {\n    let mut buf = [0u8; 10];\n    buf.pattern_fill(b\"foo\");\n    assert_eq!(&buf, b\"foofoofoof\");\n    buf.pattern_fill(b\"Hello, World!\");\n    assert_eq!(&buf, b\"Hello, Wor\");\n}\n"
  },
  {
    "path": "src/source.rs",
    "content": "use std::{\n    fs::File,\n    io::{Read, Stdin},\n};\n\n#[derive(Debug)]\npub enum SourceProvider {\n    File(File),\n    Stdin(Stdin),\n    #[cfg(windows)]\n    WinProc {\n        handle: windows_sys::Win32::Foundation::HANDLE,\n        start: usize,\n        size: usize,\n    },\n}\n\n/// FIXME: Prove this is actually safe\n#[cfg(windows)]\nunsafe impl Send for SourceProvider {}\n\n#[derive(Debug)]\npub struct Source {\n    pub provider: SourceProvider,\n    pub attr: SourceAttributes,\n    pub state: SourceState,\n}\n\nimpl Source {\n    pub fn file(f: File) -> Self {\n        Self {\n            provider: SourceProvider::File(f),\n            attr: SourceAttributes {\n                stream: false,\n                permissions: SourcePermissions { write: true },\n            },\n            state: SourceState::default(),\n        }\n    }\n}\n\n#[derive(Debug)]\npub struct SourceAttributes {\n    /// Whether reading should be done by streaming\n    pub stream: bool,\n    pub permissions: SourcePermissions,\n}\n\n#[derive(Debug, Default)]\npub struct SourceState {\n    /// Whether streaming has finished\n    pub stream_end: bool,\n}\n\n#[derive(Debug)]\npub struct SourcePermissions {\n    pub write: bool,\n}\n\nimpl Clone for SourceProvider {\n    #[expect(\n        clippy::unwrap_used,\n        reason = \"Can't really do much else in clone impl\"\n    )]\n    fn clone(&self) -> Self {\n        match self {\n            Self::File(file) => Self::File(file.try_clone().unwrap()),\n            Self::Stdin(_) => Self::Stdin(std::io::stdin()),\n            #[cfg(windows)]\n            Self::WinProc {\n                handle,\n                start,\n                size,\n            } => Self::WinProc {\n                handle: *handle,\n                start: *start,\n                size: *size,\n            },\n        }\n    }\n}\n\nimpl Read for SourceProvider {\n    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {\n        match self {\n            Self::File(f) => f.read(buf),\n            Self::Stdin(stdin) => stdin.read(buf),\n            #[cfg(windows)]\n            SourceProvider::WinProc { .. } => {\n                gamedebug_core::per!(\"Todo: Read unimplemented\");\n                Ok(0)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/str_ext.rs",
    "content": "pub trait StrExt {\n    fn is_empty_or_ws_only(&self) -> bool;\n}\n\nimpl StrExt for str {\n    fn is_empty_or_ws_only(&self) -> bool {\n        self.trim().is_empty()\n    }\n}\n"
  },
  {
    "path": "src/struct_meta_item.rs",
    "content": "use serde::{Deserialize, Serialize};\n\n#[derive(Serialize, Deserialize, Clone)]\npub struct StructMetaItem {\n    pub name: String,\n    pub src: String,\n    pub fields: Vec<StructField>,\n}\n\nimpl StructMetaItem {\n    pub fn new(parsed: structparse::Struct, src: String) -> anyhow::Result<Self> {\n        let fields: anyhow::Result<Vec<StructField>> =\n            parsed.fields.into_iter().map(try_resolve_field).collect();\n        Ok(Self {\n            name: parsed.name.to_string(),\n            src,\n            fields: fields?,\n        })\n    }\n    pub fn fields_with_offsets_mut(&mut self) -> impl Iterator<Item = (usize, &mut StructField)> {\n        let mut offset = 0;\n        let mut fields = self.fields.iter_mut();\n        std::iter::from_fn(move || {\n            let field = fields.next()?;\n            let ty_size = field.ty.size();\n            let item = (offset, &mut *field);\n            offset += ty_size;\n            Some(item)\n        })\n    }\n}\n\nfn try_resolve_field(field: structparse::Field) -> anyhow::Result<StructField> {\n    Ok(StructField {\n        name: field.name.to_string(),\n        ty: try_resolve_ty(field.ty)?,\n    })\n}\n\nfn try_resolve_ty(ty: structparse::Ty) -> anyhow::Result<StructTy> {\n    match ty {\n        structparse::Ty::Ident(ident) => {\n            let prim = match ident {\n                \"i8\" => StructPrimitive::I8,\n                \"u8\" => StructPrimitive::U8,\n                \"i16\" => StructPrimitive::I16,\n                \"u16\" => StructPrimitive::U16,\n                \"i32\" => StructPrimitive::I32,\n                \"u32\" => StructPrimitive::U32,\n                \"i64\" => StructPrimitive::I64,\n                \"u64\" => StructPrimitive::U64,\n                \"f32\" => StructPrimitive::F32,\n                \"f64\" => StructPrimitive::F64,\n                _ => anyhow::bail!(\"Unknown type\"),\n            };\n            Ok(StructTy::Primitive {\n                ty: prim,\n                endian: Endian::Le,\n            })\n        }\n        structparse::Ty::Array(array) => Ok(StructTy::Array {\n            item_ty: Box::new(try_resolve_ty(*array.ty)?),\n            len: array.len.try_into()?,\n        }),\n    }\n}\n\n#[derive(Serialize, Deserialize, Clone)]\npub struct StructField {\n    pub name: String,\n    pub ty: StructTy,\n}\n\n#[derive(Serialize, Deserialize, Clone, Copy)]\npub enum Endian {\n    Le,\n    Be,\n}\n\nimpl Endian {\n    pub fn label(&self) -> &'static str {\n        match self {\n            Self::Le => \"le\",\n            Self::Be => \"be\",\n        }\n    }\n\n    pub(crate) fn toggle(&mut self) {\n        *self = match self {\n            Self::Le => Self::Be,\n            Self::Be => Self::Le,\n        }\n    }\n}\n\n#[derive(Serialize, Deserialize, Clone)]\npub enum StructTy {\n    Primitive { ty: StructPrimitive, endian: Endian },\n    Array { item_ty: Box<Self>, len: usize },\n}\n\n#[derive(Serialize, Deserialize, Clone)]\npub enum StructPrimitive {\n    I8,\n    U8,\n    I16,\n    U16,\n    I32,\n    U32,\n    I64,\n    U64,\n    F32,\n    F64,\n}\n\nimpl StructPrimitive {\n    fn label(&self) -> &'static str {\n        match self {\n            Self::I8 => \"i8\",\n            Self::U8 => \"u8\",\n            Self::I16 => \"i16\",\n            Self::U16 => \"u16\",\n            Self::I32 => \"i32\",\n            Self::U32 => \"u32\",\n            Self::I64 => \"i64\",\n            Self::U64 => \"u64\",\n            Self::F32 => \"f32\",\n            Self::F64 => \"f64\",\n        }\n    }\n}\n\nimpl StructTy {\n    pub fn size(&self) -> usize {\n        match self {\n            Self::Primitive { ty, .. } => match ty {\n                StructPrimitive::I8 | StructPrimitive::U8 => 1,\n                StructPrimitive::I16 | StructPrimitive::U16 => 2,\n                StructPrimitive::I32 | StructPrimitive::U32 | StructPrimitive::F32 => 4,\n                StructPrimitive::I64 | StructPrimitive::U64 | StructPrimitive::F64 => 8,\n            },\n            Self::Array { item_ty, len } => item_ty.size() * *len,\n        }\n    }\n    pub fn endian_mut(&mut self) -> &mut Endian {\n        match self {\n            Self::Primitive { endian, .. } => endian,\n            Self::Array { item_ty, .. } => item_ty.endian_mut(),\n        }\n    }\n}\n\nimpl std::fmt::Display for StructTy {\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n        match self {\n            Self::Primitive { ty, endian } => {\n                let ty = ty.label();\n                let endian = endian.label();\n                write!(f, \"{ty}-{endian}\")\n            }\n            Self::Array { item_ty, len } => {\n                write!(f, \"[{item_ty}; {len}]\")\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/timer.rs",
    "content": "use std::time::{Duration, Instant};\n\n#[derive(Debug)]\npub struct Timer {\n    init_point: Instant,\n    duration: Duration,\n}\n\nimpl Timer {\n    pub fn set(duration: Duration) -> Self {\n        Self {\n            init_point: Instant::now(),\n            duration,\n        }\n    }\n    pub fn overtime(&self) -> Option<Duration> {\n        let elapsed = self.init_point.elapsed();\n        if elapsed > self.duration {\n            None\n        } else {\n            Some(elapsed)\n        }\n    }\n}\n\nimpl Default for Timer {\n    fn default() -> Self {\n        Self::set(Duration::ZERO)\n    }\n}\n"
  },
  {
    "path": "src/update.rs",
    "content": "use {\n    crate::{\n        app::{App, interact_mode::InteractMode},\n        damage_region::DamageRegion,\n        gui::{\n            self, Gui,\n            dialogs::JumpDialog,\n            message_dialog::{Icon, MessageDialog},\n            root_ctx_menu::{ContextMenu, ContextMenuData},\n        },\n        meta::{self, MetaLow, NamedView, region::Region},\n        shell::{self, msg_if_fail},\n        view::{self, ViewportVec, try_conv_mp_zero},\n    },\n    egui_file_dialog::DialogState,\n    egui_sf2g::{\n        SfEgui,\n        sf2g::{\n            graphics::{\n                Color, Font, Rect, RenderStates, RenderTarget as _, RenderWindow, Text, Vertex,\n                View,\n            },\n            window::{Event, Key, mouse},\n        },\n    },\n    gamedebug_core::per,\n    mlua::Lua,\n    slotmap::Key as _,\n};\n\n#[must_use = \"Returns false if application should quit\"]\npub fn do_frame(\n    app: &mut App,\n    gui: &mut Gui,\n    sf_egui: &mut SfEgui,\n    window: &mut RenderWindow,\n    vertex_buffer: &mut Vec<Vertex>,\n    lua: &Lua,\n    font: &Font,\n) -> anyhow::Result<bool> {\n    let font_size = 14;\n    #[expect(\n        clippy::cast_possible_truncation,\n        clippy::cast_sign_loss,\n        reason = \"It's extremely unlikely that the line spacing is not between 0..u16::MAX\"\n    )]\n    let line_spacing = font.line_spacing(u32::from(font_size)) as u16;\n    // Handle window events\n    let post_egui_evs = handle_events(gui, app, window, sf_egui, font_size, line_spacing);\n    update(app, sf_egui.context().egui_wants_keyboard_input());\n    app.update(gui, window, lua, font_size, line_spacing);\n    let mp: ViewportVec = try_conv_mp_zero(window.mouse_position());\n    let (di, cont) = gui::do_egui(\n        sf_egui,\n        gui,\n        app,\n        mp,\n        lua,\n        window,\n        font_size,\n        line_spacing,\n        font,\n    )?;\n    handle_post_egui_events(post_egui_evs, gui, app, sf_egui);\n    if !cont {\n        return Ok(false);\n    }\n    // Here we flush GUI command queue every frame\n    gui.flush_command_queue();\n    let [r, g, b] = app.preferences.bg_color;\n    #[expect(\n        clippy::cast_possible_truncation,\n        clippy::cast_sign_loss,\n        reason = \"These should be in 0-1 range, and it's just bg color. Not that important.\"\n    )]\n    window.clear(Color::rgb(\n        (r * 255.) as u8,\n        (g * 255.) as u8,\n        (b * 255.) as u8,\n    ));\n    draw(app, gui, window, font, vertex_buffer);\n    if let Some((offs, view_key)) = app.byte_offset_at_pos(mp.x, mp.y) {\n        if let Some(bm) = app.meta_state.meta.bookmarks.iter().find(|bm| bm.offset == offs) {\n            let mut txt = Text::new(bm.label.clone(), font, 20);\n            txt.tf.position = [f32::from(mp.x), f32::from(mp.y + 15)];\n            txt.draw(window, &RenderStates::DEFAULT);\n        }\n        // Mouse drag selection\n        if let Some(a) = app.hex_ui.lmb_drag_offset\n            && offs != a\n        {\n            if app.input.key_down(Key::LAlt) {\n                // Block multi-selection\n                block_select(app, view_key, a, offs);\n            } else {\n                app.hex_ui.extra_selections.clear();\n                app.hex_ui.select_a = Some(a);\n                app.hex_ui.select_b = Some(offs);\n            }\n        }\n    }\n    sf_egui.draw(di, window, None);\n    window.display();\n    gamedebug_core::inc_frame();\n    if app.quit_requested {\n        return Ok(false);\n    }\n    Ok(true)\n}\n\n/// Some events need to be handled after the egui passes, so we can know if\n/// egui wanted the keyboard or pointer input.\nfn handle_post_egui_events(\n    post_egui_evs: Vec<Event>,\n    gui: &mut Gui,\n    app: &mut App,\n    sf_egui: &SfEgui,\n) {\n    let wants_pointer = sf_egui.context().egui_wants_pointer_input();\n    for ev in post_egui_evs {\n        match ev {\n            Event::MouseButtonPressed { button, x, y } if !wants_pointer => {\n                let mp = try_conv_mp_zero((x, y));\n                if app.hex_ui.current_layout.is_null() {\n                    continue;\n                }\n                if button == mouse::Button::Left {\n                    gui.context_menu = None;\n                    if let Some((off, _view_idx)) = app.byte_offset_at_pos(mp.x, mp.y) {\n                        app.hex_ui.lmb_drag_offset = Some(off);\n                        app.edit_state.set_cursor(off);\n                    }\n                    if let Some(view_idx) = app.view_idx_at_pos(mp.x, mp.y) {\n                        app.hex_ui.focused_view = Some(view_idx);\n                        gui.win.views.selected = view_idx;\n                    }\n                } else if button == mouse::Button::Right {\n                    match app.view_at_pos(mp.x, mp.y) {\n                        Some(view_key) => match app.view_byte_offset_at_pos(view_key, mp.x, mp.y) {\n                            Some(pos) => {\n                                gui.context_menu = Some(ContextMenu::new(\n                                    mp.x,\n                                    mp.y,\n                                    ContextMenuData {\n                                        view: Some(view_key),\n                                        byte_off: Some(pos),\n                                    },\n                                ));\n                            }\n                            None => {\n                                gui.context_menu = Some(ContextMenu::new(\n                                    mp.x,\n                                    mp.y,\n                                    ContextMenuData {\n                                        view: Some(view_key),\n                                        byte_off: None,\n                                    },\n                                ));\n                            }\n                        },\n                        None => {\n                            gui.context_menu = Some(ContextMenu::new(\n                                mp.x,\n                                mp.y,\n                                ContextMenuData {\n                                    view: None,\n                                    byte_off: None,\n                                },\n                            ));\n                        }\n                    }\n                }\n            }\n            Event::MouseButtonReleased {\n                button: mouse::Button::Left,\n                ..\n            } => {\n                app.hex_ui.lmb_drag_offset = None;\n            }\n            _ => {}\n        }\n    }\n}\n\nfn update(app: &mut App, egui_wants_kb: bool) {\n    app.try_read_stream();\n    if app.data.is_empty() {\n        return;\n    }\n    app.hex_ui.show_alt_overlay = app.input.key_down(Key::LAlt);\n    if !egui_wants_kb\n        && app.hex_ui.interact_mode == InteractMode::View\n        && !app.input.key_down(Key::LControl)\n    {\n        let Some(key) = app.hex_ui.focused_view else {\n            return;\n        };\n        let spd = if app.input.key_down(Key::LShift) {\n            10\n        } else {\n            1\n        };\n        if app.input.key_down(Key::Left) {\n            app.meta_state.meta.views[key].view.scroll_x(-spd);\n        } else if app.input.key_down(Key::Right) {\n            app.meta_state.meta.views[key].view.scroll_x(spd);\n        }\n        if app.input.key_down(Key::Up) {\n            app.meta_state.meta.views[key].view.scroll_y(-spd);\n        } else if app.input.key_down(Key::Down) {\n            app.meta_state.meta.views[key].view.scroll_y(spd);\n        }\n    }\n    // Sync all other views to active view\n    if let Some(key) = app.hex_ui.focused_view {\n        let src = &app.meta_state.meta.views[key].view;\n        let src_perspective = src.perspective;\n        let (src_row, src_col) = (src.scroll_offset.row(), src.scroll_offset.col());\n        let (src_yoff, src_xoff) = (src.scroll_offset.pix_yoff(), src.scroll_offset.pix_xoff());\n        let (src_row_h, src_col_w) = (src.row_h, src.col_w);\n        for NamedView { view, name: _ } in app.meta_state.meta.views.values_mut() {\n            // Only sync views that have the same perspective\n            if view.perspective != src_perspective {\n                continue;\n            }\n            view.sync_to(src_row, src_yoff, src_col, src_xoff, src_row_h, src_col_w);\n            // Also clamp view ranges\n            if view.scroll_offset.row == 0 && view.scroll_offset.pix_yoff < 0 {\n                view.scroll_offset.pix_yoff = 0;\n            }\n            if view.scroll_offset.col == 0 && view.scroll_offset.pix_xoff < 0 {\n                view.scroll_offset.pix_xoff = 0;\n            }\n            let Some(per) = &app.meta_state.meta.low.perspectives.get(view.perspective) else {\n                per!(\"View doesn't have a perspective. Probably a bug.\");\n                continue;\n            };\n            if view.cols() < 0 {\n                per!(\"view.cols for some reason is less than 0. Probably a bug.\");\n                return;\n            }\n            if view.scroll_offset.col + 1 > per.cols {\n                view.scroll_offset.col = per.cols - 1;\n                view.scroll_offset.pix_xoff = 0;\n            }\n            if view.scroll_offset.row + 1 > per.n_rows(&app.meta_state.meta.low.regions) {\n                view.scroll_offset.row =\n                    per.n_rows(&app.meta_state.meta.low.regions).saturating_sub(1);\n                view.scroll_offset.pix_yoff = 0;\n            }\n        }\n    }\n}\n\nfn draw(\n    app: &App,\n    gui: &Gui,\n    window: &mut RenderWindow,\n    font: &Font,\n    vertex_buffer: &mut Vec<Vertex>,\n) {\n    if app.hex_ui.current_layout.is_null() {\n        let mut t = Text::new(\"No active layout\".into(), font, 20);\n        t.tf.position = [\n            f32::from(app.hex_ui.hex_iface_rect.x),\n            f32::from(app.hex_ui.hex_iface_rect.y),\n        ];\n        t.draw(window, &RenderStates::DEFAULT);\n        return;\n    }\n    for view_key in app.meta_state.meta.layouts[app.hex_ui.current_layout].iter() {\n        view::View::draw(view_key, app, gui, window, vertex_buffer, font);\n    }\n}\n\n/// Returns events that should be processed post-egui\n#[must_use]\nfn handle_events(\n    gui: &mut Gui,\n    app: &mut App,\n    window: &mut RenderWindow,\n    sf_egui: &mut SfEgui,\n    font_size: u16,\n    line_spacing: u16,\n) -> Vec<Event> {\n    let mut post_egui = Vec::new();\n    while let Some(event) = window.poll_event() {\n        let egui_ctx = sf_egui.context();\n        let wants_pointer = egui_ctx.egui_wants_pointer_input();\n        let wants_kb = egui_ctx.egui_wants_keyboard_input()\n            || matches!(gui.fileops.dialog.state(), DialogState::Open);\n        let block_event_from_egui = (matches!(event, Event::KeyPressed { code: Key::Tab, .. })\n            && !(wants_kb || wants_pointer));\n        if !block_event_from_egui {\n            sf_egui.add_event(&event);\n        }\n        if wants_kb {\n            if event == Event::Closed {\n                window.close();\n            }\n            app.input.clear();\n            continue;\n        }\n        app.input.update_from_event(&event);\n        match event {\n            Event::Closed => window.close(),\n            Event::KeyPressed {\n                code,\n                shift,\n                ctrl,\n                alt,\n                ..\n            } => handle_key_pressed(\n                code,\n                gui,\n                app,\n                KeyMod { ctrl, shift, alt },\n                wants_kb,\n                font_size,\n                line_spacing,\n            ),\n            Event::TextEntered { unicode } => {\n                handle_text_entered(app, unicode, &mut gui.msg_dialog);\n            }\n            Event::MouseButtonPressed { .. } | Event::MouseButtonReleased { .. } => {\n                post_egui.push(event);\n            }\n            Event::LostFocus => {\n                // When alt-tabbing, keys held down can get \"stuck\", because the key release events won't reach us\n                app.input.clear();\n            }\n            Event::Resized {\n                mut width,\n                mut height,\n            } => {\n                const MIN_WINDOW_W: u32 = 920;\n                const MIN_WINDOW_H: u32 = 620;\n\n                let mut needs_window_resize = false;\n                if width < MIN_WINDOW_W {\n                    width = MIN_WINDOW_W;\n                    needs_window_resize = true;\n                }\n                if height < MIN_WINDOW_H {\n                    height = MIN_WINDOW_H;\n                    needs_window_resize = true;\n                }\n                if needs_window_resize {\n                    window.set_size((width, height));\n                }\n                #[expect(\n                    clippy::cast_precision_loss,\n                    reason = \"Window sizes larger than i16::MAX aren't supported.\"\n                )]\n                match View::from_rect(Rect::new(0., 0., width as f32, height as f32)) {\n                    Ok(view) => window.set_view(&view),\n                    Err(e) => {\n                        gamedebug_core::per!(\"Failed to create view: {e}\");\n                    }\n                }\n            }\n            _ => {}\n        }\n    }\n    post_egui\n}\n\nfn handle_text_entered(app: &mut App, unicode: char, msg: &mut MessageDialog) {\n    if Key::LControl.is_pressed() || Key::LAlt.is_pressed() {\n        return;\n    }\n    match app.hex_ui.interact_mode {\n        InteractMode::Edit => {\n            let Some(focused) = app.hex_ui.focused_view else {\n                return;\n            };\n            let view = &mut app.meta_state.meta.views[focused].view;\n            view.handle_text_entered(\n                unicode,\n                &mut app.edit_state,\n                &app.preferences,\n                &mut app.data,\n                msg,\n            );\n            keep_cursor_in_view(view, &app.meta_state.meta.low, app.edit_state.cursor);\n        }\n        InteractMode::View => {}\n    }\n}\n\nstruct KeyMod {\n    ctrl: bool,\n    shift: bool,\n    alt: bool,\n}\n\nfn handle_key_pressed(\n    code: Key,\n    gui: &mut Gui,\n    app: &mut App,\n    key_mod: KeyMod,\n    egui_wants_kb: bool,\n    font_size: u16,\n    line_spacing: u16,\n) {\n    if code == Key::F12 && !key_mod.shift && !key_mod.ctrl && !key_mod.alt {\n        gamedebug_core::IMMEDIATE.toggle();\n        gamedebug_core::PERSISTENT.toggle();\n    }\n    if egui_wants_kb {\n        return;\n    }\n    // Key bindings that should work without any file open\n    match code {\n        Key::O if key_mod.ctrl => {\n            gui.fileops.load_file(app.source_file());\n        }\n        _ => {}\n    }\n    if app.data.is_empty() {\n        return;\n    }\n    let editing_text = app.hex_ui.interact_mode == InteractMode::Edit\n        && app.focused_view_mut().is_some_and(|(_k, view)| view.kind.is_text());\n    // Key bindings that should only work with a file open\n    match code {\n        Key::Up => match app.hex_ui.interact_mode {\n            InteractMode::View => {\n                if key_mod.ctrl\n                    && let Some(view_key) = app.hex_ui.focused_view\n                {\n                    let key = app.meta_state.meta.views[view_key].view.perspective;\n                    let reg = &mut app.meta_state.meta.low.regions\n                        [app.meta_state.meta.low.perspectives[key].region]\n                        .region;\n                    reg.begin = reg.begin.saturating_sub(1);\n                }\n            }\n            InteractMode::Edit => {\n                if let Some(view_key) = app.hex_ui.focused_view {\n                    let view = &mut app.meta_state.meta.views[view_key].view;\n                    view.undirty_edit_buffer();\n                    app.edit_state.set_cursor_no_history(app.edit_state.cursor.saturating_sub(\n                        app.meta_state.meta.low.perspectives[view.perspective].cols,\n                    ));\n                    keep_cursor_in_view(view, &app.meta_state.meta.low, app.edit_state.cursor);\n                }\n            }\n        },\n        Key::Down => match app.hex_ui.interact_mode {\n            InteractMode::View => {\n                if key_mod.ctrl\n                    && let Some(view_key) = app.hex_ui.focused_view\n                {\n                    let key = app.meta_state.meta.views[view_key].view.perspective;\n                    app.meta_state.meta.low.regions\n                        [app.meta_state.meta.low.perspectives[key].region]\n                        .region\n                        .begin += 1;\n                }\n            }\n            InteractMode::Edit => {\n                if let Some(view_key) = app.hex_ui.focused_view {\n                    let view = &mut app.meta_state.meta.views[view_key].view;\n                    view.undirty_edit_buffer();\n                    if app.edit_state.cursor\n                        + app.meta_state.meta.low.perspectives[view.perspective].cols\n                        < app.data.len()\n                    {\n                        app.edit_state.offset_cursor(\n                            app.meta_state.meta.low.perspectives[view.perspective].cols,\n                        );\n                    }\n                    keep_cursor_in_view(view, &app.meta_state.meta.low, app.edit_state.cursor);\n                }\n            }\n        },\n        Key::Left => 'block: {\n            if key_mod.alt {\n                app.cursor_history_back();\n                break 'block;\n            }\n            if app.hex_ui.interact_mode == InteractMode::Edit {\n                let move_edit = (app.preferences.move_edit_cursor && !key_mod.ctrl)\n                    || (!app.preferences.move_edit_cursor && key_mod.ctrl);\n                if let Some(view_key) = app.hex_ui.focused_view {\n                    let view = &mut app.meta_state.meta.views[view_key];\n                    if move_edit {\n                        if let Some(edit_buf) = view.view.edit_buffer_mut()\n                            && !edit_buf.move_cursor_back()\n                        {\n                            edit_buf.move_cursor_end();\n                            edit_buf.dirty = false;\n                            app.edit_state.step_cursor_back();\n                        }\n                    } else {\n                        app.edit_state.step_cursor_back();\n                        keep_cursor_in_view(\n                            &mut view.view,\n                            &app.meta_state.meta.low,\n                            app.edit_state.cursor,\n                        );\n                    }\n                }\n            } else if key_mod.ctrl {\n                if key_mod.shift {\n                    app.halve_cols();\n                } else {\n                    app.dec_cols();\n                }\n            }\n        }\n        Key::Right => 'block: {\n            if key_mod.alt {\n                app.cursor_history_forward();\n                break 'block;\n            }\n            if app.hex_ui.interact_mode == InteractMode::Edit\n                && app.edit_state.cursor + 1 < app.data.len()\n            {\n                let move_edit = (app.preferences.move_edit_cursor && !key_mod.ctrl)\n                    || (!app.preferences.move_edit_cursor && key_mod.ctrl);\n                if let Some(view_key) = app.hex_ui.focused_view {\n                    let view = &mut app.meta_state.meta.views[view_key];\n                    if move_edit {\n                        if let Some(edit_buf) = &mut view.view.edit_buffer_mut()\n                            && !edit_buf.move_cursor_forward()\n                        {\n                            edit_buf.move_cursor_begin();\n                            edit_buf.dirty = false;\n                            app.edit_state.step_cursor_forward();\n                        }\n                    } else {\n                        app.edit_state.step_cursor_forward();\n                        keep_cursor_in_view(\n                            &mut view.view,\n                            &app.meta_state.meta.low,\n                            app.edit_state.cursor,\n                        );\n                    }\n                }\n            } else if key_mod.ctrl {\n                if key_mod.shift {\n                    app.double_cols();\n                } else {\n                    app.inc_cols();\n                }\n            }\n        }\n        Key::PageUp => {\n            if let Some(key) = app.hex_ui.focused_view {\n                let view = &mut app.meta_state.meta.views[key].view;\n                let per = &app.meta_state.meta.low.perspectives[view.perspective];\n                match app.hex_ui.interact_mode {\n                    InteractMode::View => {\n                        view.scroll_page_up();\n                    }\n                    InteractMode::Edit => {\n                        #[expect(clippy::cast_sign_loss, reason = \"view::rows is never negative\")]\n                        {\n                            app.edit_state.cursor = app\n                                .edit_state\n                                .cursor\n                                .saturating_sub(view.rows() as usize * per.cols);\n                        }\n                        keep_cursor_in_view(view, &app.meta_state.meta.low, app.edit_state.cursor);\n                    }\n                }\n            }\n        }\n        Key::PageDown => {\n            if let Some(key) = app.hex_ui.focused_view {\n                let view = &mut app.meta_state.meta.views[key].view;\n                let per = &app.meta_state.meta.low.perspectives[view.perspective];\n                match app.hex_ui.interact_mode {\n                    InteractMode::View => {\n                        app.meta_state.meta.views[key].view.scroll_page_down();\n                    }\n                    InteractMode::Edit => {\n                        #[expect(clippy::cast_sign_loss, reason = \"view::rows is never negative\")]\n                        {\n                            app.edit_state.cursor = app\n                                .edit_state\n                                .cursor\n                                .saturating_add(view.rows() as usize * per.cols);\n                        }\n                        keep_cursor_in_view(view, &app.meta_state.meta.low, app.edit_state.cursor);\n                    }\n                }\n            }\n        }\n        Key::Home => {\n            if let Some(key) = app.hex_ui.focused_view {\n                let view = &mut app.meta_state.meta.views[key].view;\n                match app.hex_ui.interact_mode {\n                    InteractMode::View if key_mod.ctrl => {\n                        view.go_home();\n                    }\n                    InteractMode::View => {\n                        view.go_home_col();\n                    }\n                    InteractMode::Edit if key_mod.ctrl => {\n                        view.go_home();\n                        app.edit_state.cursor = app.meta_state.meta.low.start_offset_of_view(view);\n                    }\n                    InteractMode::Edit => {\n                        if let Some(row_start) = app.find_row_start(app.edit_state.cursor) {\n                            app.edit_state.cursor = row_start;\n                            keep_cursor_in_view(\n                                &mut app.meta_state.meta.views[key].view,\n                                &app.meta_state.meta.low,\n                                app.edit_state.cursor,\n                            );\n                        }\n                    }\n                }\n            }\n        }\n        Key::End => {\n            if let Some(key) = app.hex_ui.focused_view {\n                let view = &mut app.meta_state.meta.views[key].view;\n                match app.hex_ui.interact_mode {\n                    InteractMode::View if key_mod.ctrl => {\n                        view.scroll_to_end(&app.meta_state.meta.low);\n                    }\n                    InteractMode::View => {\n                        view.scroll_right_until_bump(&app.meta_state.meta.low);\n                    }\n                    InteractMode::Edit if key_mod.ctrl => {\n                        app.edit_state.cursor = app.meta_state.meta.low.end_offset_of_view(view);\n                        app.center_view_on_offset(app.edit_state.cursor);\n                    }\n                    InteractMode::Edit => {\n                        if let Some(row_end) = app.find_row_end(app.edit_state.cursor) {\n                            app.edit_state.cursor = row_end;\n                            keep_cursor_in_view(\n                                &mut app.meta_state.meta.views[key].view,\n                                &app.meta_state.meta.low,\n                                app.edit_state.cursor,\n                            );\n                        }\n                    }\n                }\n            }\n        }\n        Key::Delete => {\n            let mut any = false;\n            for sel in app.hex_ui.selected_regions() {\n                app.data.zero_fill_region(sel);\n                any = true;\n            }\n            if !any && let Some(byte) = app.data.get_mut(app.edit_state.cursor) {\n                *byte = 0;\n                app.data.widen_dirty_region(DamageRegion::Single(app.edit_state.cursor));\n            }\n        }\n        Key::F1 => app.hex_ui.interact_mode = InteractMode::View,\n        Key::F2 => app.hex_ui.interact_mode = InteractMode::Edit,\n        Key::F5 => gui.win.layouts.open.toggle(),\n        Key::F6 => gui.win.views.open.toggle(),\n        Key::F7 => gui.win.perspectives.open.toggle(),\n        Key::F8 => gui.win.regions.open.toggle(),\n        Key::F9 => gui.win.bookmarks.open.toggle(),\n        Key::F10 => gui.win.vars.open.toggle(),\n        Key::F11 => gui.win.structs.open.toggle(),\n        Key::Escape => {\n            gui.context_menu = None;\n            if let Some(view_key) = app.hex_ui.focused_view {\n                app.meta_state.meta.views[view_key].view.cancel_editing();\n            }\n            app.hex_ui.clear_selections();\n        }\n        Key::Enter => {\n            if let Some(view_key) = app.hex_ui.focused_view {\n                app.meta_state.meta.views[view_key].view.finish_editing(\n                    &mut app.edit_state,\n                    &mut app.data,\n                    &app.preferences,\n                    &mut gui.msg_dialog,\n                );\n            }\n        }\n        Key::A if key_mod.ctrl => {\n            app.focused_view_select_all();\n        }\n        Key::E if key_mod.ctrl => {\n            gui.win.external_command.open.set(true);\n        }\n        Key::F if key_mod.ctrl => {\n            gui.win.find.open.toggle();\n        }\n        Key::S if key_mod.ctrl => match &mut app.source {\n            Some(source) => {\n                if !source.attr.permissions.write {\n                    gui.msg_dialog.open(\n                        Icon::Warn,\n                        \"Cannot save\",\n                        \"This source cannot be written to.\",\n                    );\n                } else {\n                    msg_if_fail(\n                        app.save(&mut gui.msg_dialog),\n                        \"Failed to save\",\n                        &mut gui.msg_dialog,\n                    );\n                }\n            }\n            None => gui.msg_dialog.open(Icon::Warn, \"Cannot save\", \"No source opened\"),\n        },\n        Key::M if key_mod.ctrl => {\n            msg_if_fail(\n                app.save_meta(),\n                \"Failed to save metafile\",\n                &mut gui.msg_dialog,\n            );\n        }\n        Key::R if key_mod.ctrl => {\n            msg_if_fail(app.reload(), \"Failed to reload\", &mut gui.msg_dialog);\n        }\n        Key::P if key_mod.ctrl => {\n            let mut load = None;\n            shell::open_previous(app, &mut load);\n            if let Some(args) = load {\n                app.load_file_args(\n                    args,\n                    None,\n                    &mut gui.msg_dialog,\n                    font_size,\n                    line_spacing,\n                    None,\n                );\n            }\n        }\n        Key::W if key_mod.ctrl => app.close_file(),\n        Key::J if key_mod.ctrl => Gui::add_dialog(&mut gui.dialogs, JumpDialog::default()),\n        Key::Num1 if key_mod.shift => {\n            if !editing_text {\n                app.hex_ui.select_a = Some(app.edit_state.cursor);\n            }\n        }\n        Key::Num2 if key_mod.shift => {\n            if !editing_text {\n                app.hex_ui.select_b = Some(app.edit_state.cursor);\n            }\n        }\n        // Block selection with alt+1/2\n        Key::Num1 if key_mod.alt => {\n            if let Some(b) = app.hex_ui.select_b\n                && let Some((view_key, _)) = app.focused_view_mut()\n            {\n                block_select(app, view_key, app.edit_state.cursor, b);\n            } else {\n                app.hex_ui.select_a = Some(app.edit_state.cursor);\n            }\n        }\n        Key::Num2 if key_mod.alt => {\n            if let Some(a) = app.hex_ui.select_a\n                && let Some((view_key, _)) = app.focused_view_mut()\n            {\n                block_select(app, view_key, app.edit_state.cursor, a);\n            } else {\n                app.hex_ui.select_b = Some(app.edit_state.cursor);\n            }\n        }\n        Key::Tab if key_mod.shift => app.focus_prev_view_in_layout(),\n        Key::Tab => app.focus_next_view_in_layout(),\n        Key::Equal if key_mod.ctrl => app.inc_byte_or_bytes(),\n        Key::Hyphen if key_mod.ctrl => app.dec_byte_or_bytes(),\n        _ => {}\n    }\n}\n\nfn keep_cursor_in_view(view: &mut view::View, meta_low: &MetaLow, cursor: usize) {\n    let view_offs = view.offsets(&meta_low.perspectives, &meta_low.regions);\n    let [cur_row, cur_col] =\n        meta_low.perspectives[view.perspective].row_col_of_byte_offset(cursor, &meta_low.regions);\n    view.scroll_offset.pix_xoff = 0;\n    view.scroll_offset.pix_yoff = 0;\n    if view_offs.row > cur_row {\n        view.scroll_offset.row = cur_row;\n    }\n    #[expect(clippy::cast_sign_loss, reason = \"rows is always unsigned\")]\n    let view_rows = view.rows() as usize;\n    if (view_offs.row + view_rows) < cur_row.saturating_add(1) {\n        view.scroll_offset.row = (cur_row + 1) - view_rows;\n    }\n    if view_offs.col > cur_col {\n        view.scroll_offset.col = cur_col;\n    }\n    #[expect(clippy::cast_sign_loss, reason = \"cols is always unsigned\")]\n    let view_cols = view.cols() as usize;\n    if (view_offs.col + view_cols + 1) < cur_col {\n        view.scroll_offset.col = (cur_col - view_cols) + 1;\n    }\n}\n\nfn block_select(app: &mut App, view_key: meta::ViewKey, a: usize, b: usize) {\n    let view = &app.meta_state.meta.views[view_key];\n    let per = &app.meta_state.meta.low.perspectives[view.view.perspective];\n    let [a_row, a_col] = per.row_col_of_byte_offset(a, &app.meta_state.meta.low.regions);\n    let [b_row, b_col] = per.row_col_of_byte_offset(b, &app.meta_state.meta.low.regions);\n    let [min_row, max_row] = std::cmp::minmax(a_row, b_row);\n    let [min_col, max_col] = std::cmp::minmax(a_col, b_col);\n    let mut rows = min_row..=max_row;\n    if let Some(row) = rows.next() {\n        let a = per.byte_offset_of_row_col(row, min_col, &app.meta_state.meta.low.regions);\n        app.hex_ui.select_a = Some(a);\n        let b = per.byte_offset_of_row_col(row, max_col, &app.meta_state.meta.low.regions);\n        app.hex_ui.select_b = Some(b);\n    }\n    app.hex_ui.extra_selections.clear();\n    for row in rows {\n        let a = per.byte_offset_of_row_col(row, min_col, &app.meta_state.meta.low.regions);\n        let b = per.byte_offset_of_row_col(row, max_col, &app.meta_state.meta.low.regions);\n        app.hex_ui.extra_selections.push(Region { begin: a, end: b });\n    }\n}\n"
  },
  {
    "path": "src/util.rs",
    "content": "#[expect(\n    clippy::cast_precision_loss,\n    reason = \"This is just an approximation of data size\"\n)]\npub fn human_size(size: usize) -> String {\n    human_bytes::human_bytes(size as f64)\n}\n\n#[expect(\n    clippy::cast_precision_loss,\n    reason = \"This is just an approximation of data size\"\n)]\npub fn human_size_u64(size: u64) -> String {\n    human_bytes::human_bytes(size as f64)\n}\n"
  },
  {
    "path": "src/value_color.rs",
    "content": "use {\n    crate::color::{RgbColor, rgb},\n    serde::{Deserialize, Serialize},\n    serde_big_array::BigArray,\n    std::path::Path,\n};\n\n#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]\npub enum ColorMethod {\n    Mono(RgbColor),\n    Default,\n    Pure,\n    Rgb332,\n    Vga13h,\n    BrightScale(RgbColor),\n    Custom(Box<Palette>),\n}\n\n#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]\npub struct Palette(#[serde(with = \"BigArray\")] pub [[u8; 3]; 256]);\n\npub fn load_palette(path: &Path) -> anyhow::Result<Palette> {\n    let raw_bytes = std::fs::read(path)?;\n    if raw_bytes.len() != size_of::<Palette>() {\n        anyhow::bail!(\"File for palette not the correct size\");\n    }\n    let mut pal = Palette([[0u8; 3]; 256]);\n    for (rgb, pal_slot) in raw_bytes.as_chunks::<3>().0.iter().zip(pal.0.iter_mut()) {\n        *pal_slot = *rgb;\n    }\n    Ok(pal)\n}\n\npub fn save_palette(pal: &Palette, path: &Path) -> anyhow::Result<()> {\n    let raw_bytes: &[u8] = pal.0.as_flattened();\n    Ok(std::fs::write(path, raw_bytes)?)\n}\n\nimpl ColorMethod {\n    #[must_use]\n    pub fn byte_color(&self, byte: u8, invert: bool) -> RgbColor {\n        let color = match self {\n            Self::Mono(color) => *color,\n            Self::Default => default_color(byte),\n            Self::Pure => hue_color(byte),\n            Self::Rgb332 => rgb332_color(byte),\n            Self::Vga13h => vga_13h_color(byte),\n            Self::BrightScale(color) => color.cap_brightness(byte),\n            Self::Custom(pal) => {\n                let [r, g, b] = pal.0[byte as usize];\n                rgb(r, g, b)\n            }\n        };\n        if invert { color.invert() } else { color }\n    }\n\n    pub(crate) fn name(&self) -> &str {\n        match self {\n            Self::Mono(_) => \"monochrome\",\n            Self::Default => \"default\",\n            Self::Pure => \"pure hue\",\n            Self::Rgb332 => \"rgb 3-3-2\",\n            Self::Vga13h => \"VGA 13h\",\n            Self::BrightScale(_) => \"brightness scale\",\n            Self::Custom(_) => \"custom\",\n        }\n    }\n}\n\nfn vga_13h_color(byte: u8) -> RgbColor {\n    let c24 = VGA_13H_PALETTE[byte as usize];\n    let r = c24 >> 16;\n    let g = c24 >> 8;\n    let b = c24;\n    #[expect(\n        clippy::cast_possible_truncation,\n        reason = \"This is just playing around with colors. Non-critical.\"\n    )]\n    rgb(r as u8, g as u8, b as u8)\n}\n\nfn rgb332_color(byte: u8) -> RgbColor {\n    let r = byte & 0b11100000;\n    let g = byte & 0b00011100;\n    let b = byte & 0b00000011;\n    rgb((r >> 5) * 32, (g >> 2) * 32, b * 64)\n}\n\nconst VGA_13H_PALETTE: [u32; 256] = [\n    0x000000, 0x0000a8, 0x00a800, 0x00a8a8, 0xa80000, 0xa800a8, 0xa85400, 0xa8a8a8, 0x545454,\n    0x5454fc, 0x54fc54, 0x54fcfc, 0xfc5454, 0xfc54fc, 0xfcfc54, 0xfcfcfc, 0x000000, 0x141414,\n    0x202020, 0x2c2c2c, 0x383838, 0x444444, 0x505050, 0x606060, 0x707070, 0x808080, 0x909090,\n    0xa0a0a0, 0xb4b4b4, 0xc8c8c8, 0xe0e0e0, 0xfcfcfc, 0x0000fc, 0x4000fc, 0x7c00fc, 0xbc00fc,\n    0xfc00fc, 0xfc00bc, 0xfc007c, 0xfc0040, 0xfc0000, 0xfc4000, 0xfc7c00, 0xfcbc00, 0xfcfc00,\n    0xbcfc00, 0x7cfc00, 0x40fc00, 0x00fc00, 0x00fc40, 0x00fc7c, 0x00fcbc, 0x00fcfc, 0x00bcfc,\n    0x007cfc, 0x0040fc, 0x7c7cfc, 0x9c7cfc, 0xbc7cfc, 0xdc7cfc, 0xfc7cfc, 0xfc7cdc, 0xfc7cbc,\n    0xfc7c9c, 0xfc7c7c, 0xfc9c7c, 0xfcbc7c, 0xfcdc7c, 0xfcfc7c, 0xdcfc7c, 0xbcfc7c, 0x9cfc7c,\n    0x7cfc7c, 0x7cfc9c, 0x7cfcbc, 0x7cfcdc, 0x7cfcfc, 0x7cdcfc, 0x7cbcfc, 0x7c9cfc, 0xb4b4fc,\n    0xc4b4fc, 0xd8b4fc, 0xe8b4fc, 0xfcb4fc, 0xfcb4e8, 0xfcb4d8, 0xfcb4c4, 0xfcb4b4, 0xfcc4b4,\n    0xfcd8b4, 0xfce8b4, 0xfcfcb4, 0xe8fcb4, 0xd8fcb4, 0xc4fcb4, 0xb4fcb4, 0xb4fcc4, 0xb4fcd8,\n    0xb4fce8, 0xb4fcfc, 0xb4e8fc, 0xb4d8fc, 0xb4c4fc, 0x000070, 0x1c0070, 0x380070, 0x540070,\n    0x700070, 0x700054, 0x700038, 0x70001c, 0x700000, 0x701c00, 0x703800, 0x705400, 0x707000,\n    0x547000, 0x387000, 0x1c7000, 0x007000, 0x00701c, 0x007038, 0x007054, 0x007070, 0x005470,\n    0x003870, 0x001c70, 0x383870, 0x443870, 0x543870, 0x603870, 0x703870, 0x703860, 0x703854,\n    0x703844, 0x703838, 0x704438, 0x705438, 0x706038, 0x707038, 0x607038, 0x547038, 0x447038,\n    0x387038, 0x387044, 0x387054, 0x387060, 0x387070, 0x386070, 0x385470, 0x384470, 0x505070,\n    0x585070, 0x605070, 0x685070, 0x705070, 0x705068, 0x705060, 0x705058, 0x705050, 0x705850,\n    0x706050, 0x706850, 0x707050, 0x687050, 0x607050, 0x587050, 0x507050, 0x507058, 0x507060,\n    0x507068, 0x507070, 0x506870, 0x506070, 0x505870, 0x000040, 0x100040, 0x200040, 0x300040,\n    0x400040, 0x400030, 0x400020, 0x400010, 0x400000, 0x401000, 0x402000, 0x403000, 0x404000,\n    0x304000, 0x204000, 0x104000, 0x004000, 0x004010, 0x004020, 0x004030, 0x004040, 0x003040,\n    0x002040, 0x001040, 0x202040, 0x282040, 0x302040, 0x382040, 0x402040, 0x402038, 0x402030,\n    0x402028, 0x402020, 0x402820, 0x403020, 0x403820, 0x404020, 0x384020, 0x304020, 0x284020,\n    0x204020, 0x204028, 0x204030, 0x204038, 0x204040, 0x203840, 0x203040, 0x202840, 0x2c2c40,\n    0x302c40, 0x342c40, 0x3c2c40, 0x402c40, 0x402c3c, 0x402c34, 0x402c30, 0x402c2c, 0x40302c,\n    0x40342c, 0x403c2c, 0x40402c, 0x3c402c, 0x34402c, 0x30402c, 0x2c402c, 0x2c4030, 0x2c4034,\n    0x2c403c, 0x2c4040, 0x2c3c40, 0x2c3440, 0x2c3040, 0x000000, 0x000000, 0x000000, 0x000000,\n    0x000000, 0x000000, 0x000000, 0x000000,\n];\n\npub fn default_color(byte: u8) -> RgbColor {\n    DEFAULT_COLOR_ARRAY[usize::from(byte)]\n}\n\nfn hue_color(byte: u8) -> RgbColor {\n    let [r, g, b] = egui::ecolor::rgb_from_hsv((f32::from(byte) / 288.0, 1.0, 1.0));\n    #[expect(\n        clippy::cast_possible_truncation,\n        clippy::cast_sign_loss,\n        reason = \"Ranges are in 0-1, they will never be multiplied above 255\"\n    )]\n    rgb((r * 255.0) as u8, (g * 255.0) as u8, (b * 255.0) as u8)\n}\n\n#[expect(dead_code, reason = \"DEFAULT_COLOR_ARRAY is generated based on this\")]\nfn hue_color_tweaked(byte: u8) -> RgbColor {\n    if byte == 0 {\n        rgb(100, 100, 100)\n    } else if byte == 255 {\n        rgb(210, 210, 210)\n    } else {\n        hue_color(byte)\n    }\n}\n\n/// Color table for default_color. This is used for performance purposes, as it is\n/// expensive to calculate the default colors.\nconst DEFAULT_COLOR_ARRAY: [RgbColor; 256] = [\n    rgb(100, 100, 100),\n    rgb(255, 5, 0),\n    rgb(255, 10, 0),\n    rgb(255, 15, 0),\n    rgb(255, 21, 0),\n    rgb(255, 26, 0),\n    rgb(255, 31, 0),\n    rgb(255, 37, 0),\n    rgb(255, 42, 0),\n    rgb(255, 47, 0),\n    rgb(255, 53, 0),\n    rgb(255, 58, 0),\n    rgb(255, 63, 0),\n    rgb(255, 69, 0),\n    rgb(255, 74, 0),\n    rgb(255, 79, 0),\n    rgb(255, 85, 0),\n    rgb(255, 90, 0),\n    rgb(255, 95, 0),\n    rgb(255, 100, 0),\n    rgb(255, 106, 0),\n    rgb(255, 111, 0),\n    rgb(255, 116, 0),\n    rgb(255, 122, 0),\n    rgb(255, 127, 0),\n    rgb(255, 132, 0),\n    rgb(255, 138, 0),\n    rgb(255, 143, 0),\n    rgb(255, 148, 0),\n    rgb(255, 154, 0),\n    rgb(255, 159, 0),\n    rgb(255, 164, 0),\n    rgb(255, 170, 0),\n    rgb(255, 175, 0),\n    rgb(255, 180, 0),\n    rgb(255, 185, 0),\n    rgb(255, 191, 0),\n    rgb(255, 196, 0),\n    rgb(255, 201, 0),\n    rgb(255, 207, 0),\n    rgb(255, 212, 0),\n    rgb(255, 217, 0),\n    rgb(255, 223, 0),\n    rgb(255, 228, 0),\n    rgb(255, 233, 0),\n    rgb(255, 239, 0),\n    rgb(255, 244, 0),\n    rgb(255, 249, 0),\n    rgb(255, 254, 0),\n    rgb(249, 255, 0),\n    rgb(244, 255, 0),\n    rgb(239, 255, 0),\n    rgb(233, 255, 0),\n    rgb(228, 255, 0),\n    rgb(223, 255, 0),\n    rgb(217, 255, 0),\n    rgb(212, 255, 0),\n    rgb(207, 255, 0),\n    rgb(201, 255, 0),\n    rgb(196, 255, 0),\n    rgb(191, 255, 0),\n    rgb(185, 255, 0),\n    rgb(180, 255, 0),\n    rgb(175, 255, 0),\n    rgb(170, 255, 0),\n    rgb(164, 255, 0),\n    rgb(159, 255, 0),\n    rgb(154, 255, 0),\n    rgb(148, 255, 0),\n    rgb(143, 255, 0),\n    rgb(138, 255, 0),\n    rgb(132, 255, 0),\n    rgb(127, 255, 0),\n    rgb(122, 255, 0),\n    rgb(116, 255, 0),\n    rgb(111, 255, 0),\n    rgb(106, 255, 0),\n    rgb(100, 255, 0),\n    rgb(95, 255, 0),\n    rgb(90, 255, 0),\n    rgb(84, 255, 0),\n    rgb(79, 255, 0),\n    rgb(74, 255, 0),\n    rgb(69, 255, 0),\n    rgb(63, 255, 0),\n    rgb(58, 255, 0),\n    rgb(53, 255, 0),\n    rgb(47, 255, 0),\n    rgb(42, 255, 0),\n    rgb(37, 255, 0),\n    rgb(31, 255, 0),\n    rgb(26, 255, 0),\n    rgb(21, 255, 0),\n    rgb(15, 255, 0),\n    rgb(10, 255, 0),\n    rgb(5, 255, 0),\n    rgb(0, 255, 0),\n    rgb(0, 255, 5),\n    rgb(0, 255, 10),\n    rgb(0, 255, 15),\n    rgb(0, 255, 21),\n    rgb(0, 255, 26),\n    rgb(0, 255, 31),\n    rgb(0, 255, 37),\n    rgb(0, 255, 42),\n    rgb(0, 255, 47),\n    rgb(0, 255, 53),\n    rgb(0, 255, 58),\n    rgb(0, 255, 63),\n    rgb(0, 255, 69),\n    rgb(0, 255, 74),\n    rgb(0, 255, 79),\n    rgb(0, 255, 84),\n    rgb(0, 255, 90),\n    rgb(0, 255, 95),\n    rgb(0, 255, 100),\n    rgb(0, 255, 106),\n    rgb(0, 255, 111),\n    rgb(0, 255, 116),\n    rgb(0, 255, 122),\n    rgb(0, 255, 127),\n    rgb(0, 255, 132),\n    rgb(0, 255, 138),\n    rgb(0, 255, 143),\n    rgb(0, 255, 148),\n    rgb(0, 255, 154),\n    rgb(0, 255, 159),\n    rgb(0, 255, 164),\n    rgb(0, 255, 169),\n    rgb(0, 255, 175),\n    rgb(0, 255, 180),\n    rgb(0, 255, 185),\n    rgb(0, 255, 191),\n    rgb(0, 255, 196),\n    rgb(0, 255, 201),\n    rgb(0, 255, 207),\n    rgb(0, 255, 212),\n    rgb(0, 255, 217),\n    rgb(0, 255, 223),\n    rgb(0, 255, 228),\n    rgb(0, 255, 233),\n    rgb(0, 255, 239),\n    rgb(0, 255, 244),\n    rgb(0, 255, 249),\n    rgb(0, 255, 255),\n    rgb(0, 249, 255),\n    rgb(0, 244, 255),\n    rgb(0, 239, 255),\n    rgb(0, 233, 255),\n    rgb(0, 228, 255),\n    rgb(0, 223, 255),\n    rgb(0, 217, 255),\n    rgb(0, 212, 255),\n    rgb(0, 207, 255),\n    rgb(0, 201, 255),\n    rgb(0, 196, 255),\n    rgb(0, 191, 255),\n    rgb(0, 185, 255),\n    rgb(0, 180, 255),\n    rgb(0, 175, 255),\n    rgb(0, 169, 255),\n    rgb(0, 164, 255),\n    rgb(0, 159, 255),\n    rgb(0, 154, 255),\n    rgb(0, 148, 255),\n    rgb(0, 143, 255),\n    rgb(0, 138, 255),\n    rgb(0, 132, 255),\n    rgb(0, 127, 255),\n    rgb(0, 122, 255),\n    rgb(0, 116, 255),\n    rgb(0, 111, 255),\n    rgb(0, 106, 255),\n    rgb(0, 100, 255),\n    rgb(0, 95, 255),\n    rgb(0, 90, 255),\n    rgb(0, 84, 255),\n    rgb(0, 79, 255),\n    rgb(0, 74, 255),\n    rgb(0, 69, 255),\n    rgb(0, 63, 255),\n    rgb(0, 58, 255),\n    rgb(0, 53, 255),\n    rgb(0, 47, 255),\n    rgb(0, 42, 255),\n    rgb(0, 37, 255),\n    rgb(0, 31, 255),\n    rgb(0, 26, 255),\n    rgb(0, 21, 255),\n    rgb(0, 15, 255),\n    rgb(0, 10, 255),\n    rgb(0, 5, 255),\n    rgb(0, 0, 255),\n    rgb(5, 0, 255),\n    rgb(10, 0, 255),\n    rgb(15, 0, 255),\n    rgb(21, 0, 255),\n    rgb(26, 0, 255),\n    rgb(31, 0, 255),\n    rgb(37, 0, 255),\n    rgb(42, 0, 255),\n    rgb(47, 0, 255),\n    rgb(53, 0, 255),\n    rgb(58, 0, 255),\n    rgb(63, 0, 255),\n    rgb(69, 0, 255),\n    rgb(74, 0, 255),\n    rgb(79, 0, 255),\n    rgb(84, 0, 255),\n    rgb(90, 0, 255),\n    rgb(95, 0, 255),\n    rgb(100, 0, 255),\n    rgb(106, 0, 255),\n    rgb(111, 0, 255),\n    rgb(116, 0, 255),\n    rgb(122, 0, 255),\n    rgb(127, 0, 255),\n    rgb(132, 0, 255),\n    rgb(138, 0, 255),\n    rgb(143, 0, 255),\n    rgb(148, 0, 255),\n    rgb(154, 0, 255),\n    rgb(159, 0, 255),\n    rgb(164, 0, 255),\n    rgb(170, 0, 255),\n    rgb(175, 0, 255),\n    rgb(180, 0, 255),\n    rgb(185, 0, 255),\n    rgb(191, 0, 255),\n    rgb(196, 0, 255),\n    rgb(201, 0, 255),\n    rgb(207, 0, 255),\n    rgb(212, 0, 255),\n    rgb(217, 0, 255),\n    rgb(223, 0, 255),\n    rgb(228, 0, 255),\n    rgb(233, 0, 255),\n    rgb(239, 0, 255),\n    rgb(244, 0, 255),\n    rgb(249, 0, 255),\n    rgb(254, 0, 255),\n    rgb(255, 0, 249),\n    rgb(255, 0, 244),\n    rgb(255, 0, 239),\n    rgb(255, 0, 233),\n    rgb(255, 0, 228),\n    rgb(255, 0, 223),\n    rgb(255, 0, 217),\n    rgb(255, 0, 212),\n    rgb(255, 0, 207),\n    rgb(255, 0, 201),\n    rgb(255, 0, 196),\n    rgb(255, 0, 191),\n    rgb(255, 0, 185),\n    rgb(255, 0, 180),\n    rgb(210, 210, 210),\n];\n"
  },
  {
    "path": "src/view/draw.rs",
    "content": "use {\n    super::View,\n    crate::{\n        app::{App, presentation::Presentation},\n        color::RgbColor,\n        dec_conv,\n        gui::Gui,\n        hex_conv,\n        hex_ui::HexUi,\n        meta::{PerspectiveMap, RegionMap, ViewKey, region::Region},\n        struct_meta_item::StructMetaItem,\n        view::ViewKind,\n    },\n    egui_sf2g::sf2g::{\n        graphics::{\n            Color, Font, PrimitiveType, RenderStates, RenderTarget as _, RenderWindow, Text, Vertex,\n        },\n        system::Vector2,\n    },\n    either::Either,\n    slotmap::Key as _,\n};\n\nstruct DrawArgs<'vert, 'data> {\n    vertices: &'vert mut Vec<Vertex>,\n    x: f32,\n    y: f32,\n    data: &'data [u8],\n    idx: usize,\n    color: RgbColor,\n    highlight: bool,\n}\n\nfn draw_view<'f>(\n    view: &View,\n    key: ViewKey,\n    app_perspectives: &PerspectiveMap,\n    app_regions: &RegionMap,\n    app_structs: &[StructMetaItem],\n    app_data: &[u8],\n    app_hex_ui: &HexUi,\n    app_ui: &Gui,\n    vertex_buffer: &mut Vec<Vertex>,\n    overlay_texts: &mut Vec<Text<'f>>,\n    font: &'f Font,\n    mut drawfn: impl FnMut(DrawArgs),\n) {\n    // Protect against infinite loop lock up when scrolling horizontally out of view\n    if view.scroll_offset.pix_xoff <= -view.viewport_rect.w || view.perspective.is_null() {\n        return;\n    }\n    let perspective = &app_perspectives[view.perspective];\n    let region = &app_regions[perspective.region].region;\n    let mut idx = region.begin;\n    let start_row: usize = view.scroll_offset.row;\n    idx += start_row * (perspective.cols * usize::from(view.bytes_per_block));\n    #[expect(\n        clippy::cast_sign_loss,\n        reason = \"rows() returning negative is a bug, should be positive.\"\n    )]\n    let orig = start_row..=start_row + view.rows() as usize;\n    let (row_range, pix_yoff) = if perspective.flip_row_order {\n        (Either::Left(orig.rev()), -view.scroll_offset.pix_yoff)\n    } else {\n        (Either::Right(orig), view.scroll_offset.pix_yoff)\n    };\n    'rows: for row in row_range {\n        let y = row * usize::from(view.row_h);\n        #[expect(\n            clippy::cast_possible_wrap,\n            reason = \"Files bigger than i64::MAX aren't supported\"\n        )]\n        let viewport_y = (i64::from(view.viewport_rect.y) + y as i64)\n            - ((view.scroll_offset.row as i64 * i64::from(view.row_h)) + i64::from(pix_yoff));\n        let start_col = view.scroll_offset.col;\n        if start_col >= perspective.cols {\n            break;\n        }\n        idx += start_col * usize::from(view.bytes_per_block);\n        for col in start_col..perspective.cols {\n            let x = col * usize::from(view.col_w);\n            #[expect(\n                clippy::cast_possible_wrap,\n                reason = \"Files bigger than i64::MAX aren't supported\"\n            )]\n            let viewport_x = (i64::from(view.viewport_rect.x) + x as i64)\n                - ((view.scroll_offset.col as i64 * i64::from(view.col_w))\n                    + i64::from(view.scroll_offset.pix_xoff));\n            if viewport_x > i64::from(view.viewport_rect.x + view.viewport_rect.w) {\n                idx += (perspective.cols - col) * usize::from(view.bytes_per_block);\n                break;\n            }\n            if idx > region.end {\n                break 'rows;\n            }\n            if viewport_y > i64::from(view.viewport_rect.y + view.viewport_rect.h)\n                && !perspective.flip_row_order\n            {\n                break 'rows;\n            }\n            match app_data.get(idx..idx + view.bytes_per_block as usize) {\n                Some(data) => {\n                    let c = view\n                        .presentation\n                        .color_method\n                        .byte_color(data[0], view.presentation.invert_color);\n                    #[expect(\n                        clippy::cast_precision_loss,\n                        reason = \"At this point, the viewport coordinates should be small enough to fit in viewport\"\n                    )]\n                    drawfn(DrawArgs {\n                        vertices: vertex_buffer,\n                        x: viewport_x as f32,\n                        y: viewport_y as f32,\n                        data,\n                        idx,\n                        color: c,\n                        highlight: should_highlight(app_hex_ui.selected_regions(), idx, app_ui),\n                    });\n                    /*if gamedebug_core::enabled() {\n                        #[expect(\n                            clippy::cast_precision_loss,\n                            reason = \"At this point, the viewport coordinates should be small enough to fit in viewport\"\n                        )]\n                        draw_rect_outline(\n                            vertex_buffer,\n                            viewport_x as f32,\n                            viewport_y as f32,\n                            view.col_w.into(),\n                            view.row_h.into(),\n                            Color::RED,\n                            -1.0,\n                        );\n                    }*/\n                    idx += usize::from(view.bytes_per_block);\n                }\n                None => {\n                    if !perspective.flip_row_order {\n                        break 'rows;\n                    }\n                }\n            }\n        }\n    }\n    if let Some(ruler) = app_hex_ui.rulers.get(&key)\n        && ruler.freq != 0\n    {\n        let y = view.viewport_rect.y;\n        let h = view.viewport_rect.h;\n        let base_x = view.viewport_rect.x;\n        let view_p_cols = view.p_cols(app_perspectives);\n        let view_cols =\n            usize::try_from(view.cols()).expect(\"Bug: view.cols() returned negative number\");\n        let end = view_p_cols.min(view.scroll_offset.col + view_cols);\n        // TODO: Hacky \"gap\" calculation to try to make rulers look \"good\" by default\n        // Needs proper way to determine \"center of gap between columns\",\n        // so we can place the vertical lines there\n        #[expect(clippy::cast_possible_truncation)]\n        let gap = (f64::from(view.col_w) * 0.17) as i16;\n        match ruler.struct_idx {\n            Some(idx) => {\n                let Some(struct_) = app_structs.get(idx) else {\n                    gamedebug_core::per!(\"Dangling struct index: {idx}\");\n                    return;\n                };\n                let mut col = 0;\n                for (i, field) in struct_.fields.iter().enumerate() {\n                    // Draw field names if alt overlay is enabled\n                    // TODO: Very hacky, needs proper support in the future\n                    if app_hex_ui.show_alt_overlay\n                        && let Some(line_x) = line_x(view, col)\n                    {\n                        let mut text = Text::new(field.name.clone(), font, 12);\n                        text.set_outline_thickness(1.0);\n                        text.set_fill_color(Color::WHITE);\n                        text.set_outline_color(Color::BLACK);\n                        let y_offs = [48.0, 72.0, 96.0];\n                        let y_off = y_offs[i % y_offs.len()];\n                        let x = base_x + line_x + ruler.hoffset;\n                        text.tf.position = [f32::from(x), f32::from(y) + y_off];\n                        overlay_texts.push(text);\n                    }\n                    col += field.ty.size();\n                    let Some(line_x) = line_x(view, col) else {\n                        continue;\n                    };\n                    let x = (base_x + line_x + ruler.hoffset) - gap;\n                    draw_vline(\n                        vertex_buffer,\n                        f32::from(x),\n                        f32::from(y),\n                        f32::from(h),\n                        ruler.color.into(),\n                    );\n                }\n            }\n            None => {\n                for col in view.scroll_offset.col..end {\n                    if col.is_multiple_of(usize::from(ruler.freq)) {\n                        // We want to draw the line after the current column\n                        let col = col + 1;\n                        let x_offset = i16::try_from(col - view.scroll_offset.col)\n                            .expect(\"Bug: x offset larger than i16::MAX\");\n                        let line_x = (x_offset\n                            * i16::try_from(view.col_w).expect(\"Bug: col_w larger than i16::MAX\"))\n                            - view.scroll_offset.pix_xoff;\n                        let x = (base_x + line_x + ruler.hoffset) - gap;\n                        draw_vline(\n                            vertex_buffer,\n                            f32::from(x),\n                            f32::from(y),\n                            f32::from(h),\n                            ruler.color.into(),\n                        );\n                    }\n                }\n            }\n        }\n    }\n}\n\nfn line_x(view: &View, col: usize) -> Option<i16> {\n    let x_off = col.checked_sub(view.scroll_offset.col)?;\n    let Ok(x_offset) = i16::try_from(x_off) else {\n        gamedebug_core::per!(\"Bug: x offset ({x_off}) larger than i16::MAX\");\n        return None;\n    };\n    let line_x = (x_offset * i16::try_from(view.col_w).expect(\"Bug: col_w larger than i16::MAX\"))\n        - view.scroll_offset.pix_xoff;\n    Some(line_x)\n}\n\nfn draw_text_cursor(\n    x: f32,\n    y: f32,\n    vertices: &mut Vec<Vertex>,\n    active: bool,\n    flash_timer: Option<u32>,\n    presentation: &Presentation,\n    font_size: u16,\n) {\n    let color = cursor_color(active, flash_timer, presentation);\n    draw_rect_outline(\n        vertices,\n        x,\n        y,\n        f32::from(font_size / 2),\n        f32::from(font_size),\n        color,\n        -2.0,\n    );\n}\n\nfn draw_block_cursor(\n    x: f32,\n    y: f32,\n    vertices: &mut Vec<Vertex>,\n    active: bool,\n    flash_timer: Option<u32>,\n    presentation: &Presentation,\n    view: &View,\n) {\n    let color = cursor_color(active, flash_timer, presentation);\n    draw_rect(\n        vertices,\n        x,\n        y,\n        f32::from(view.col_w),\n        f32::from(view.row_h),\n        color,\n    );\n}\n\n#[expect(\n    clippy::cast_possible_truncation,\n    reason = \"Deliberate color modulation based on timer value.\"\n)]\nfn cursor_color(active: bool, flash_timer: Option<u32>, presentation: &Presentation) -> Color {\n    if active {\n        match flash_timer {\n            Some(timer) => Color::rgb(timer as u8, timer as u8, timer as u8),\n            None => presentation.cursor_active_color.into(),\n        }\n    } else {\n        match flash_timer {\n            Some(timer) => Color::rgb(timer as u8, timer as u8, timer as u8),\n            None => presentation.cursor_color.into(),\n        }\n    }\n}\n\n#[expect(\n    clippy::cast_precision_loss,\n    reason = \"These casts deal with texture rect coords.\n              These aren't expected to be larger than what fits into f32\"\n)]\nfn draw_glyph(\n    font: &Font,\n    font_size: u32,\n    vertices: &mut Vec<Vertex>,\n    mut x: f32,\n    mut y: f32,\n    glyph: u32,\n    color: Color,\n) {\n    let glyph = font.glyph(glyph, font_size, false, 0.0);\n    let bounds = glyph.bounds();\n    let texture_rect = glyph.texture_rect();\n    let baseline = font_size as f32;\n    let offset = baseline + bounds.top;\n    x += bounds.left;\n    y += offset;\n    vertices.push(Vertex {\n        position: Vector2::new(x, y),\n        color,\n        tex_coords: texture_rect.position().as_other(),\n    });\n    vertices.push(Vertex {\n        position: Vector2::new(x, y + bounds.height),\n        color,\n        tex_coords: Vector2::new(\n            texture_rect.left as f32,\n            (texture_rect.top + texture_rect.height) as f32,\n        ),\n    });\n    vertices.push(Vertex {\n        position: Vector2::new(x + bounds.width, y + bounds.height),\n        color,\n        tex_coords: Vector2::new(\n            (texture_rect.left + texture_rect.width) as f32,\n            (texture_rect.top + texture_rect.height) as f32,\n        ),\n    });\n    vertices.push(Vertex {\n        position: Vector2::new(x + bounds.width, y),\n        color,\n        tex_coords: Vector2::new(\n            (texture_rect.left + texture_rect.width) as f32,\n            texture_rect.top as f32,\n        ),\n    });\n}\n\nfn draw_rect(vertices: &mut Vec<Vertex>, x: f32, y: f32, w: f32, h: f32, color: Color) {\n    vertices.extend([\n        Vertex {\n            position: Vector2::new(x, y),\n            color,\n            tex_coords: Vector2::default(),\n        },\n        Vertex {\n            position: Vector2::new(x, y + h),\n            color,\n            tex_coords: Vector2::default(),\n        },\n        Vertex {\n            position: Vector2::new(x + w, y + h),\n            color,\n            tex_coords: Vector2::default(),\n        },\n        Vertex {\n            position: Vector2::new(x + w, y),\n            color,\n            tex_coords: Vector2::default(),\n        },\n    ]);\n}\n\nfn draw_vline(vertices: &mut Vec<Vertex>, x: f32, y: f32, h: f32, color: Color) {\n    vertices.extend([\n        Vertex {\n            position: Vector2::new(x, y),\n            color,\n            tex_coords: Vector2::default(),\n        },\n        Vertex {\n            position: Vector2::new(x, y + h),\n            color,\n            tex_coords: Vector2::default(),\n        },\n        Vertex {\n            position: Vector2::new(x + 1.0, y + h),\n            color,\n            tex_coords: Vector2::default(),\n        },\n        Vertex {\n            position: Vector2::new(x + 1.0, y),\n            color,\n            tex_coords: Vector2::default(),\n        },\n    ]);\n}\n\nfn draw_rect_outline(\n    vertices: &mut Vec<Vertex>,\n    x: f32,\n    y: f32,\n    w: f32,\n    h: f32,\n    color: Color,\n    thickness: f32,\n) {\n    // top\n    draw_rect(\n        vertices,\n        x - thickness,\n        y - thickness,\n        w + thickness,\n        thickness,\n        color,\n    );\n    // right\n    draw_rect(\n        vertices,\n        x + w,\n        y - thickness,\n        thickness,\n        h + thickness,\n        color,\n    );\n    // bottom\n    draw_rect(\n        vertices,\n        x - thickness,\n        y + h,\n        w + thickness * 2.0,\n        thickness,\n        color,\n    );\n    // left\n    draw_rect(\n        vertices,\n        x - thickness,\n        y - thickness,\n        thickness,\n        h + thickness,\n        color,\n    );\n}\n\nimpl View {\n    pub fn draw(\n        key: ViewKey,\n        app: &App,\n        gui: &Gui,\n        window: &mut RenderWindow,\n        vertex_buffer: &mut Vec<Vertex>,\n        font: &Font,\n    ) {\n        vertex_buffer.clear();\n        let mut rs = RenderStates::default();\n        let Some(this) = app.meta_state.meta.views.get(key) else {\n            return;\n        };\n        let mut overlay_texts = Vec::new();\n        match &this.view.kind {\n            ViewKind::Hex(hex) => {\n                draw_view(\n                    &this.view,\n                    key,\n                    &app.meta_state.meta.low.perspectives,\n                    &app.meta_state.meta.low.regions,\n                    &app.meta_state.meta.structs,\n                    &app.data,\n                    &app.hex_ui,\n                    gui,\n                    vertex_buffer,\n                    &mut overlay_texts,\n                    font,\n                    |DrawArgs {\n                         vertices,\n                         x,\n                         y,\n                         data,\n                         idx,\n                         color: c,\n                         highlight,\n                     }| {\n                        if highlight {\n                            draw_rect(\n                                vertices,\n                                x,\n                                y,\n                                f32::from(this.view.col_w),\n                                f32::from(this.view.row_h),\n                                this.view.presentation.sel_color.into(),\n                            );\n                        }\n                        let mut gx = x;\n                        for (i, mut d) in\n                            hex_conv::byte_to_hex_digits(data[0]).into_iter().enumerate()\n                        {\n                            if idx == app.edit_state.cursor && hex.edit_buf.dirty {\n                                d = hex.edit_buf.buf[i];\n                            }\n                            draw_glyph(\n                                font,\n                                hex.font_size.into(),\n                                vertices,\n                                gx,\n                                y,\n                                d.into(),\n                                c.into(),\n                            );\n                            gx += f32::from(hex.font_size - 4);\n                        }\n                        let extra_x = hex.edit_buf.cursor * (hex.font_size - 4);\n                        if !app.preferences.hide_cursor && idx == app.edit_state.cursor {\n                            draw_text_cursor(\n                                x + f32::from(extra_x),\n                                y,\n                                vertices,\n                                app.hex_ui.focused_view == Some(key),\n                                app.hex_ui.cursor_flash_timer(),\n                                &this.view.presentation,\n                                hex.font_size,\n                            );\n                        }\n                    },\n                );\n                rs.texture = Some(font.texture(hex.font_size.into()));\n            }\n            ViewKind::Dec(dec) => {\n                draw_view(\n                    &this.view,\n                    key,\n                    &app.meta_state.meta.low.perspectives,\n                    &app.meta_state.meta.low.regions,\n                    &app.meta_state.meta.structs,\n                    &app.data,\n                    &app.hex_ui,\n                    gui,\n                    vertex_buffer,\n                    &mut overlay_texts,\n                    font,\n                    |DrawArgs {\n                         vertices,\n                         x,\n                         y,\n                         data,\n                         idx,\n                         color: c,\n                         highlight,\n                     }| {\n                        if highlight {\n                            draw_rect(\n                                vertices,\n                                x,\n                                y,\n                                f32::from(this.view.col_w),\n                                f32::from(this.view.row_h),\n                                this.view.presentation.sel_color.into(),\n                            );\n                        }\n                        let mut gx = x;\n                        for (i, mut d) in\n                            dec_conv::byte_to_dec_digits(data[0]).into_iter().enumerate()\n                        {\n                            if idx == app.edit_state.cursor && dec.edit_buf.dirty {\n                                d = dec.edit_buf.buf[i];\n                            }\n                            draw_glyph(\n                                font,\n                                dec.font_size.into(),\n                                vertices,\n                                gx,\n                                y,\n                                d.into(),\n                                c.into(),\n                            );\n                            gx += f32::from(dec.font_size - 4);\n                        }\n                        let extra_x = dec.edit_buf.cursor * (dec.font_size - 4);\n                        if !app.preferences.hide_cursor && idx == app.edit_state.cursor {\n                            draw_text_cursor(\n                                x + f32::from(extra_x),\n                                y,\n                                vertices,\n                                app.hex_ui.focused_view == Some(key),\n                                app.hex_ui.cursor_flash_timer(),\n                                &this.view.presentation,\n                                dec.font_size,\n                            );\n                        }\n                    },\n                );\n                rs.texture = Some(font.texture(dec.font_size.into()));\n            }\n            ViewKind::Text(text) => {\n                draw_view(\n                    &this.view,\n                    key,\n                    &app.meta_state.meta.low.perspectives,\n                    &app.meta_state.meta.low.regions,\n                    &app.meta_state.meta.structs,\n                    &app.data,\n                    &app.hex_ui,\n                    gui,\n                    vertex_buffer,\n                    &mut overlay_texts,\n                    font,\n                    |DrawArgs {\n                         vertices,\n                         x,\n                         y,\n                         data,\n                         idx,\n                         color: c,\n                         highlight,\n                     }| {\n                        if highlight {\n                            draw_rect(\n                                vertices,\n                                x,\n                                y,\n                                f32::from(this.view.col_w),\n                                f32::from(this.view.row_h),\n                                this.view.presentation.sel_color.into(),\n                            );\n                        }\n                        let raw_data = match text.text_kind {\n                            crate::view::TextKind::Ascii => {\n                                u32::from(data[0].wrapping_add_signed(text.offset))\n                            }\n                            crate::view::TextKind::Utf16Le => {\n                                u32::from(u16::from_le_bytes([data[0], data[1]]))\n                            }\n                            crate::view::TextKind::Utf16Be => {\n                                u32::from(u16::from_be_bytes([data[0], data[1]]))\n                            }\n                        };\n                        let glyph = match raw_data {\n                            0x00 => '∅' as u32,\n                            0x09 => '⇥' as u32,\n                            0x0A => '⏎' as u32,\n                            0x0D => '⇤' as u32,\n                            0x20 => '␣' as u32,\n                            0xFF => '■' as u32,\n                            _ => raw_data,\n                        };\n                        draw_glyph(font, text.font_size.into(), vertices, x, y, glyph, c.into());\n                        if !app.preferences.hide_cursor && idx == app.edit_state.cursor {\n                            draw_text_cursor(\n                                x,\n                                y,\n                                vertices,\n                                app.hex_ui.focused_view == Some(key),\n                                app.hex_ui.cursor_flash_timer(),\n                                &this.view.presentation,\n                                text.font_size,\n                            );\n                        }\n                    },\n                );\n                rs.texture = Some(font.texture(text.font_size.into()));\n            }\n            ViewKind::Block => {\n                draw_view(\n                    &this.view,\n                    key,\n                    &app.meta_state.meta.low.perspectives,\n                    &app.meta_state.meta.low.regions,\n                    &app.meta_state.meta.structs,\n                    &app.data,\n                    &app.hex_ui,\n                    gui,\n                    vertex_buffer,\n                    &mut overlay_texts,\n                    font,\n                    |DrawArgs {\n                         vertices,\n                         x,\n                         y,\n                         data: _,\n                         idx,\n                         color: mut c,\n                         highlight,\n                     }| {\n                        if highlight {\n                            c = c.invert();\n                        }\n                        draw_rect(\n                            vertices,\n                            x,\n                            y,\n                            f32::from(this.view.col_w),\n                            f32::from(this.view.row_h),\n                            c.into(),\n                        );\n                        if !app.preferences.hide_cursor && idx == app.edit_state.cursor {\n                            draw_block_cursor(\n                                x,\n                                y,\n                                vertices,\n                                app.hex_ui.focused_view == Some(key),\n                                app.hex_ui.cursor_flash_timer(),\n                                &this.view.presentation,\n                                &this.view,\n                            );\n                        }\n                    },\n                );\n            }\n        }\n        draw_rect_outline(\n            vertex_buffer,\n            f32::from(this.view.viewport_rect.x - 2),\n            f32::from(this.view.viewport_rect.y - 2),\n            f32::from(this.view.viewport_rect.w + 3),\n            f32::from(this.view.viewport_rect.h + 3),\n            if Some(key) == app.hex_ui.focused_view {\n                Color::rgb(255, 255, 150)\n            } else {\n                Color::rgb(120, 120, 150)\n            },\n            -1.0,\n        );\n        if app.hex_ui.scissor_views {\n            // Safety: It's just some OpenGL calls, it's fine, trust me\n            unsafe {\n                glu_sys::glEnable(glu_sys::GL_SCISSOR_TEST);\n                #[expect(\n                    clippy::cast_possible_truncation,\n                    reason = \"Huge window sizes (>32000) are not supported.\"\n                )]\n                let vh = window.size().y as i16;\n                let [x, y, w, h] = rect_to_gl_viewport(\n                    this.view.viewport_rect.x - 2,\n                    this.view.viewport_rect.y - 2,\n                    this.view.viewport_rect.w + 3,\n                    this.view.viewport_rect.h + 3,\n                    vh,\n                );\n                glu_sys::glScissor(x, y, w, h);\n            }\n        }\n        if app.hex_ui.show_alt_overlay {\n            let per = &app.meta_state.meta.low.perspectives[this.view.perspective];\n            let mut text = Text::new(\n                format!(\n                    \"{}\\n{}x{}\",\n                    this.name,\n                    per.n_rows(&app.meta_state.meta.low.regions),\n                    per.cols\n                ),\n                font,\n                16,\n            );\n            text.tf.position = [\n                f32::from(this.view.viewport_rect.x),\n                f32::from(this.view.viewport_rect.y),\n            ];\n            let text_bounds = text.global_bounds();\n            draw_rect(\n                vertex_buffer,\n                text_bounds.left,\n                text_bounds.top,\n                text_bounds.width,\n                text_bounds.height,\n                Color::rgba(32, 32, 32, 200),\n            );\n            overlay_texts.push(text);\n        }\n        window.draw_primitives(vertex_buffer, PrimitiveType::QUADS, &rs);\n        if app.hex_ui.scissor_views {\n            // Safety: It's an innocent OpenGL call\n            unsafe {\n                glu_sys::glDisable(glu_sys::GL_SCISSOR_TEST);\n            }\n        }\n        for mut text in overlay_texts {\n            text.draw(window, &RenderStates::DEFAULT);\n        }\n    }\n}\n\nfn rect_to_gl_viewport(x: i16, y: i16, w: i16, h: i16, viewport_h: i16) -> [i32; 4] {\n    [x, viewport_h - (y + h), w, h].map(glu_sys::GLint::from)\n}\n\n#[test]\nfn test_rect_to_gl() {\n    let vh = 1080;\n    assert_eq!(rect_to_gl_viewport(0, 0, 0, 0, vh), [0, 1080, 0, 0]);\n    assert_eq!(\n        rect_to_gl_viewport(100, 480, 300, 400, vh),\n        [100, 200, 300, 400]\n    );\n}\n\nfn should_highlight(\n    mut app_selection: impl Iterator<Item = Region>,\n    idx: usize,\n    gui: &Gui,\n) -> bool {\n    app_selection.any(|reg| reg.contains(idx)) || gui.highlight_set.contains(&idx)\n}\n"
  },
  {
    "path": "src/view.rs",
    "content": "use {\n    crate::{\n        app::{edit_state::EditState, presentation::Presentation},\n        damage_region::DamageRegion,\n        data::Data,\n        edit_buffer::EditBuffer,\n        gui::message_dialog::{Icon, MessageDialog},\n        hex_conv::merge_hex_halves,\n        meta::{MetaLow, PerspectiveKey, PerspectiveMap, RegionMap, region::Region},\n        session_prefs::SessionPrefs,\n    },\n    gamedebug_core::per,\n    serde::{Deserialize, Serialize},\n    slotmap::Key as _,\n};\n\nmod draw;\n\n/// A rectangular view in the viewport looking through a perspective at the data with a flavor\n/// of rendering/interaction (hex/ascii/block/etc.)\n///\n/// There can be different views through the same perspective.\n/// By default they sync their offsets, but each view can show different amounts of data\n/// depending on block size of its items, and its relative size in the viewport.\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct View {\n    /// The rectangle to occupy in the viewport\n    #[serde(skip)]\n    pub viewport_rect: ViewportRect,\n    /// The kind of view (hex, ascii, block, etc)\n    pub kind: ViewKind,\n    /// Width of a column\n    pub col_w: u16,\n    /// Height of a row\n    pub row_h: u16,\n    /// The scrolling offset\n    #[serde(skip)]\n    pub scroll_offset: ScrollOffset,\n    /// The amount scrolled for a single scroll operation, in pixels\n    pub scroll_speed: i16,\n    /// How many bytes are required for a single block in the view\n    pub bytes_per_block: u8,\n    /// The perspective this view is associated with\n    pub perspective: PerspectiveKey,\n    /// Color schemes, etc.\n    pub presentation: Presentation,\n}\n\nimpl PartialEq for View {\n    fn eq(&self, other: &Self) -> bool {\n        self.kind == other.kind\n            && self.col_w == other.col_w\n            && self.row_h == other.row_h\n            && self.scroll_speed == other.scroll_speed\n            && self.bytes_per_block == other.bytes_per_block\n            && self.presentation == other.presentation\n    }\n}\n\nimpl Eq for View {}\n\nimpl View {\n    pub fn new(kind: ViewKind, perspective: PerspectiveKey) -> Self {\n        let mut this = Self {\n            viewport_rect: ViewportRect::default(),\n            kind,\n            // TODO: Hack. We're setting this to 4, 4 to avoid zeroed default block view.\n            // Solve in a better way.\n            col_w: 4,\n            row_h: 4,\n            scroll_offset: ScrollOffset::default(),\n            scroll_speed: 0,\n            bytes_per_block: 1,\n            perspective,\n            presentation: Presentation::default(),\n        };\n        this.adjust_state_to_kind();\n        this\n    }\n    pub fn scroll_x(&mut self, amount: i16) {\n        #[expect(\n            clippy::cast_possible_wrap,\n            reason = \"block size is never greater than i16::MAX\"\n        )]\n        scroll_impl(\n            &mut self.scroll_offset.col,\n            &mut self.scroll_offset.pix_xoff,\n            self.col_w as i16,\n            amount,\n        );\n    }\n    pub fn scroll_y(&mut self, amount: i16) {\n        #[expect(\n            clippy::cast_possible_wrap,\n            reason = \"block size is never greater than i16::MAX\"\n        )]\n        scroll_impl(\n            &mut self.scroll_offset.row,\n            &mut self.scroll_offset.pix_yoff,\n            self.row_h as i16,\n            amount,\n        );\n    }\n\n    pub(crate) fn sync_to(\n        &mut self,\n        src_row: usize,\n        src_yoff: i16,\n        src_col: usize,\n        src_xoff: i16,\n        src_row_h: u16,\n        src_col_w: u16,\n    ) {\n        self.scroll_offset.row = src_row;\n        self.scroll_offset.col = src_col;\n        let y_ratio = f64::from(src_row_h) / f64::from(self.row_h);\n        let x_ratio = f64::from(src_col_w) / f64::from(self.col_w);\n        #[expect(\n            clippy::cast_possible_truncation,\n            reason = \"Input values are all low (look at input types)\"\n        )]\n        {\n            self.scroll_offset.pix_yoff = (f64::from(src_yoff) / y_ratio) as i16;\n            self.scroll_offset.pix_xoff = (f64::from(src_xoff) / x_ratio) as i16;\n        }\n    }\n\n    pub(crate) fn scroll_page_down(&mut self) {\n        self.scroll_y(self.viewport_rect.h);\n    }\n\n    pub(crate) fn scroll_page_up(&mut self) {\n        self.scroll_y(-self.viewport_rect.h);\n    }\n\n    pub(crate) fn scroll_page_left(&mut self) {\n        self.scroll_x(-self.viewport_rect.w);\n    }\n\n    pub(crate) fn go_home(&mut self) {\n        self.scroll_offset.row = 0;\n        self.scroll_offset.col = 0;\n        self.scroll_offset.floor();\n    }\n\n    pub(crate) fn go_home_col(&mut self) {\n        self.scroll_offset.col = 0;\n        self.scroll_offset.pix_xoff = 0;\n    }\n\n    /// Scroll so the perspective's last row is visible\n    pub(crate) fn scroll_to_end(&mut self, meta_low: &MetaLow) {\n        // Needs:\n        // - row index of last byte of perspective\n        // - number of rows this view can hold\n        let perspective = &meta_low.perspectives[self.perspective];\n        let last_row_idx = perspective.last_row_idx(&meta_low.regions);\n        let last_col_idx = perspective.last_col_idx(&meta_low.regions);\n        self.scroll_offset.row = last_row_idx + 1;\n        self.scroll_offset.col = last_col_idx + 1;\n        self.scroll_page_up();\n        self.scroll_page_left();\n        self.scroll_offset.floor();\n    }\n    /// Scrolls the view right until it \"bumps\" into the right edge of content\n    pub(crate) fn scroll_right_until_bump(&mut self, meta_low: &MetaLow) {\n        let per = &meta_low.perspectives[self.perspective];\n        #[expect(clippy::cast_sign_loss, reason = \"self.cols() is essentially `u15`\")]\n        let view_cols = self.cols() as usize;\n        let offset = per.cols.saturating_sub(view_cols);\n        self.scroll_offset.col = offset;\n        self.scroll_offset.floor();\n    }\n\n    /// Row/col offset of relative position, including scrolling\n    pub(crate) fn row_col_offset_of_pos(\n        &self,\n        x: i16,\n        y: i16,\n        perspectives: &PerspectiveMap,\n        regions: &RegionMap,\n    ) -> Option<[usize; 2]> {\n        self.viewport_rect\n            .relative_offset_of_pos(x, y)\n            .and_then(|(x, y)| self.row_col_of_rel_pos(x, y, perspectives, regions))\n    }\n    #[expect(\n        clippy::cast_possible_wrap,\n        reason = \"block size is never greater than i16::MAX\"\n    )]\n    fn row_col_of_rel_pos(\n        &self,\n        x: i16,\n        y: i16,\n        perspectives: &PerspectiveMap,\n        regions: &RegionMap,\n    ) -> Option<[usize; 2]> {\n        let rel_x = x + self.scroll_offset.pix_xoff;\n        let rel_y = y + self.scroll_offset.pix_yoff;\n        let rel_col = rel_x / self.col_w as i16;\n        let mut rel_row = rel_y / self.row_h as i16;\n        let perspective = match perspectives.get(self.perspective) {\n            Some(per) => per,\n            None => {\n                per!(\"row_col_of_rel_pos: Invalid perspective key\");\n                return None;\n            }\n        };\n        if perspective.flip_row_order {\n            rel_row = self.rows() - rel_row;\n        }\n        let row = self.scroll_offset.row;\n        let col = self.scroll_offset.col;\n        #[expect(\n            clippy::cast_sign_loss,\n            reason = \"rel_x and rel_y being positive also ensure rel_row and rel_col are\"\n        )]\n        if rel_x.is_positive() && rel_y.is_positive() {\n            let abs_row = row + rel_row as usize;\n            let abs_col = col + rel_col as usize;\n            if perspective.row_col_within_bound(abs_row, abs_col, regions) {\n                Some([abs_row, abs_col])\n            } else {\n                None\n            }\n        } else {\n            None\n        }\n    }\n\n    pub(crate) fn center_on_offset(\n        &mut self,\n        offset: usize,\n        perspectives: &PerspectiveMap,\n        regions: &RegionMap,\n    ) {\n        let [row, col] = perspectives[self.perspective].row_col_of_byte_offset(offset, regions);\n        self.center_on_row_col(row, col);\n    }\n\n    fn center_on_row_col(&mut self, row: usize, col: usize) {\n        self.scroll_offset.row = row;\n        self.scroll_offset.col = col;\n        self.scroll_offset.floor();\n        self.scroll_x(-self.viewport_rect.w / 2);\n        self.scroll_y(-self.viewport_rect.h / 2);\n    }\n\n    pub fn offsets(&self, perspectives: &PerspectiveMap, regions: &RegionMap) -> Offsets {\n        let row = self.scroll_offset.row;\n        let col = self.scroll_offset.col;\n        Offsets {\n            row,\n            col,\n            byte: perspectives[self.perspective].byte_offset_of_row_col(row, col, regions),\n        }\n    }\n    /// Scroll to byte offset, with control of each axis individually\n    pub(crate) fn scroll_to_byte_offset(\n        &mut self,\n        offset: usize,\n        perspectives: &PerspectiveMap,\n        regions: &RegionMap,\n        do_col: bool,\n        do_row: bool,\n    ) {\n        let [row, col] = perspectives[self.perspective].row_col_of_byte_offset(offset, regions);\n        if do_row {\n            self.scroll_offset.row = row;\n        }\n        if do_col {\n            self.scroll_offset.col = col;\n        }\n        self.scroll_offset.floor();\n    }\n    #[expect(\n        clippy::cast_sign_loss,\n        reason = \"View::rows() being negative is a bug, can expect positive.\"\n    )]\n    pub(crate) fn bytes_per_page(&self, perspectives: &PerspectiveMap) -> usize {\n        (self.rows() as usize) * perspectives[self.perspective].cols\n    }\n\n    /// Returns the number of rows this view can display\n    #[expect(\n        clippy::cast_possible_wrap,\n        reason = \"block size is never greater than i16::MAX\"\n    )]\n    pub(crate) fn rows(&self) -> i16 {\n        // If the viewport rect is smaller than 0, we just return 0 for the rows\n        if self.viewport_rect.h <= 0 {\n            return 0;\n        }\n        self.viewport_rect.h / (self.row_h as i16)\n    }\n    /// Returns the number of columns this view can display visibly at once.\n    ///\n    /// This might not be the total number of columns in the perspective this view is attached to.\n    #[expect(\n        clippy::cast_possible_wrap,\n        reason = \"block size is never greater than i16::MAX\"\n    )]\n    pub(crate) fn cols(&self) -> i16 {\n        match self.viewport_rect.w.checked_div(self.col_w as i16) {\n            Some(result) => result,\n            None => {\n                per!(\"Divide by zero in View::cols. Bug.\");\n                0\n            }\n        }\n    }\n\n    /// Returns the number of columns of the perspective this view is attached to.\n    pub(crate) fn p_cols(&self, perspectives: &PerspectiveMap) -> usize {\n        match perspectives.get(self.perspective) {\n            Some(per) => per.cols,\n            None => 0,\n        }\n    }\n\n    pub fn adjust_block_size(&mut self) {\n        (self.col_w, self.row_h) = match &self.kind {\n            ViewKind::Hex(hex) => (hex.font_size * 2 - 2, hex.font_size),\n            ViewKind::Dec(dec) => (dec.font_size * 3 - 6, dec.font_size),\n            ViewKind::Text(data) => (data.font_size, data.line_spacing.max(1)),\n            ViewKind::Block => (self.col_w, self.row_h),\n        }\n    }\n    /// Adjust state after kind was changed\n    pub fn adjust_state_to_kind(&mut self) {\n        self.adjust_block_size();\n        let glyph_count = self.glyph_count();\n        match &mut self.kind {\n            ViewKind::Hex(HexData { edit_buf, .. })\n            | ViewKind::Dec(HexData { edit_buf, .. })\n            | ViewKind::Text(TextData { edit_buf, .. }) => edit_buf.resize(glyph_count),\n            _ => {}\n        }\n    }\n    /// The number of glyphs per block this view has\n    fn glyph_count(&self) -> u16 {\n        match self.kind {\n            ViewKind::Hex(_) => 2,\n            ViewKind::Dec(_) => 3,\n            ViewKind::Text { .. } => 1,\n            ViewKind::Block => 1,\n        }\n    }\n    pub fn handle_text_entered(\n        &mut self,\n        unicode: char,\n        edit_state: &mut EditState,\n        preferences: &SessionPrefs,\n        data: &mut Data,\n        msg: &mut MessageDialog,\n    ) {\n        if self.char_valid(unicode) {\n            match &mut self.kind {\n                ViewKind::Hex(hex) => {\n                    if !hex.edit_buf.dirty {\n                        let Some(byte) = data.get(edit_state.cursor) else {\n                            return;\n                        };\n                        let s = format!(\"{byte:02X}\");\n                        hex.edit_buf.update_from_string(&s);\n                    }\n                    if hex.edit_buf.enter_byte(unicode.to_ascii_uppercase() as u8)\n                        || preferences.quick_edit\n                    {\n                        self.finish_editing(edit_state, data, preferences, msg);\n                    }\n                }\n                ViewKind::Dec(dec) => {\n                    if !dec.edit_buf.dirty {\n                        let Some(byte) = data.get(edit_state.cursor) else {\n                            return;\n                        };\n                        let s = format!(\"{byte:03}\");\n                        dec.edit_buf.update_from_string(&s);\n                    }\n                    if dec.edit_buf.enter_byte(unicode.to_ascii_uppercase() as u8)\n                        || preferences.quick_edit\n                    {\n                        self.finish_editing(edit_state, data, preferences, msg);\n                    }\n                }\n                ViewKind::Text(text) => {\n                    if text.edit_buf.enter_byte((unicode as u8).wrapping_add_signed(-text.offset))\n                        || preferences.quick_edit\n                    {\n                        self.finish_editing(edit_state, data, preferences, msg);\n                    }\n                }\n                // Block doesn't do any text input\n                ViewKind::Block => {}\n            }\n        }\n    }\n\n    /// Returns the size needed by this view to display fully\n    pub fn max_needed_size(\n        &self,\n        perspectives: &PerspectiveMap,\n        regions: &RegionMap,\n    ) -> ViewportVec {\n        if self.perspective.is_null() {\n            return ViewportVec { x: 0, y: 0 };\n        }\n        let p = &perspectives[self.perspective];\n        let n_rows = p.n_rows(regions);\n        ViewportVec {\n            x: i16::saturating_from(p.cols).saturating_mul(i16::saturating_from(self.col_w)),\n            y: i16::saturating_from(n_rows).saturating_mul(i16::saturating_from(self.row_h)),\n        }\n    }\n\n    fn char_valid(&self, unicode: char) -> bool {\n        match self.kind {\n            ViewKind::Hex(_) => matches!(unicode, '0'..='9' | 'a'..='f'),\n            ViewKind::Dec(_) => unicode.is_ascii_digit(),\n            ViewKind::Text { .. } => {\n                unicode.is_ascii() && !unicode.is_control() && !matches!(unicode, '\\t')\n            }\n            ViewKind::Block => false,\n        }\n    }\n\n    pub fn finish_editing(\n        &mut self,\n        edit_state: &mut EditState,\n        data: &mut Data,\n        preferences: &SessionPrefs,\n        msg: &mut MessageDialog,\n    ) {\n        match &mut self.kind {\n            ViewKind::Hex(hex) => {\n                match merge_hex_halves(hex.edit_buf.buf[0], hex.edit_buf.buf[1]) {\n                    Some(merged) => {\n                        if let Some(byte) = data.get_mut(edit_state.cursor) {\n                            *byte = merged;\n                        }\n                    }\n                    None => per!(\"finish_editing: Failed to merge hex halves\"),\n                }\n                data.widen_dirty_region(DamageRegion::Single(edit_state.cursor));\n            }\n            ViewKind::Dec(dec) => {\n                let s =\n                    std::str::from_utf8(&dec.edit_buf.buf).expect(\"Invalid utf-8 in edit buffer\");\n                match s.parse() {\n                    Ok(num) => {\n                        data[edit_state.cursor] = num;\n                        data.widen_dirty_region(DamageRegion::Single(edit_state.cursor));\n                    }\n                    Err(e) => msg.open(Icon::Error, \"Invalid value\", e.to_string()),\n                }\n            }\n            ViewKind::Text(text) => {\n                let Some(byte) = data.get_mut(edit_state.cursor) else {\n                    return;\n                };\n                *byte = text.edit_buf.buf[0];\n                data.widen_dirty_region(DamageRegion::Single(edit_state.cursor));\n            }\n            ViewKind::Block => {}\n        }\n        if edit_state.cursor + 1 < data.len() && !preferences.sticky_edit {\n            edit_state.step_cursor_forward();\n        }\n        self.reset_edit_buf();\n    }\n\n    pub fn cancel_editing(&mut self) {\n        self.reset_edit_buf();\n    }\n\n    pub fn reset_edit_buf(&mut self) {\n        if let Some(edit_buf) = self.edit_buffer_mut() {\n            edit_buf.reset();\n        }\n    }\n\n    pub(crate) fn undirty_edit_buffer(&mut self) {\n        if let Some(edit_buf) = self.edit_buffer_mut() {\n            edit_buf.dirty = false;\n        }\n    }\n\n    pub(crate) fn edit_buffer_mut(&mut self) -> Option<&mut EditBuffer> {\n        match &mut self.kind {\n            ViewKind::Hex(data) | ViewKind::Dec(data) => Some(&mut data.edit_buf),\n            ViewKind::Text(data) => Some(&mut data.edit_buf),\n            ViewKind::Block => None,\n        }\n    }\n\n    pub(crate) fn contains_region(&self, reg: &Region, meta: &crate::meta::Meta) -> bool {\n        meta.low.regions[meta.low.perspectives[self.perspective].region]\n            .region\n            .contains_region(reg)\n    }\n}\n\ntrait SatFrom<V> {\n    fn saturating_from(src: V) -> Self;\n}\n\nimpl SatFrom<usize> for i16 {\n    fn saturating_from(src: usize) -> Self {\n        Self::try_from(src).unwrap_or(Self::MAX)\n    }\n}\n\nimpl SatFrom<u16> for i16 {\n    fn saturating_from(src: u16) -> Self {\n        Self::try_from(src).unwrap_or(Self::MAX)\n    }\n}\n\npub struct Offsets {\n    pub row: usize,\n    pub col: usize,\n    pub byte: usize,\n}\n\n/// When scrolling past 0 whole, allows unbounded negative pixel offset\nfn scroll_impl(whole: &mut usize, pixel: &mut i16, pixels_per_whole: i16, scroll_by: i16) {\n    *pixel += scroll_by;\n    if pixel.is_negative() {\n        while *pixel <= -pixels_per_whole {\n            if *whole == 0 {\n                break;\n            }\n            *whole -= 1;\n            *pixel += pixels_per_whole;\n        }\n    } else {\n        while *pixel >= pixels_per_whole {\n            *whole += 1;\n            *pixel -= pixels_per_whole;\n        }\n    }\n}\n\n#[test]\nfn test_scroll_impl_positive() {\n    let mut whole;\n    let mut pixel;\n    let px_per_whole = 32;\n    // Add 1\n    whole = 0;\n    pixel = 0;\n    scroll_impl(&mut whole, &mut pixel, px_per_whole, 1);\n    assert_eq!((whole, pixel), (0, 1));\n    // Add 1000\n    whole = 0;\n    pixel = 0;\n    scroll_impl(&mut whole, &mut pixel, px_per_whole, 1000);\n    assert_eq!((whole, pixel), (31, 8));\n    // Add 1 until we get to 1 whole\n    whole = 0;\n    pixel = 0;\n    for _ in 0..32 {\n        scroll_impl(&mut whole, &mut pixel, px_per_whole, 1);\n    }\n    assert_eq!((whole, pixel), (1, 0));\n}\n\n#[test]\nfn test_scroll_impl_negative() {\n    let mut whole;\n    let mut pixel;\n    let px_per_whole = 32;\n    // Add -1000 (negative test)\n    whole = 0;\n    pixel = 0;\n    scroll_impl(&mut whole, &mut pixel, px_per_whole, -1000);\n    assert_eq!((whole, pixel), (0, -1000));\n    // Make 10 wholes 0\n    whole = 10;\n    pixel = 0;\n    scroll_impl(&mut whole, &mut pixel, px_per_whole, -320);\n    assert_eq!((whole, pixel), (0, 0));\n    // Make 10 wholes 0, scroll remainder\n    whole = 10;\n    pixel = 0;\n    scroll_impl(&mut whole, &mut pixel, px_per_whole, -640);\n    assert_eq!((whole, pixel), (0, -320));\n}\n\n#[derive(Debug, Default, Clone, Copy)]\npub struct ScrollOffset {\n    /// What column we are at\n    pub col: usize,\n    /// Additional pixel x offset\n    pub pix_xoff: i16,\n    /// What row we are at\n    pub row: usize,\n    /// Additional pixel y offset\n    pub pix_yoff: i16,\n}\n\nimpl ScrollOffset {\n    pub fn col(&self) -> usize {\n        self.col\n    }\n    pub fn row(&self) -> usize {\n        self.row\n    }\n    pub fn pix_xoff(&self) -> i16 {\n        self.pix_xoff\n    }\n    pub fn pix_yoff(&self) -> i16 {\n        self.pix_yoff\n    }\n    /// Discard pixel offsets\n    pub(crate) fn floor(&mut self) {\n        self.pix_xoff = 0;\n        self.pix_yoff = 0;\n    }\n}\n\n/// Type for representing viewport magnitudes.\n///\n/// We assume that hexerator will never run on resolutions higher than 32767x32767,\n/// or get mouse positions higher than that.\npub type ViewportScalar = i16;\n\n#[derive(Debug, Default, Clone, Copy)]\npub struct ViewportRect {\n    pub x: ViewportScalar,\n    pub y: ViewportScalar,\n    pub w: ViewportScalar,\n    pub h: ViewportScalar,\n}\n\n#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]\npub struct ViewportVec {\n    pub x: ViewportScalar,\n    pub y: ViewportScalar,\n}\n\nimpl TryFrom<(i32, i32)> for ViewportVec {\n    type Error = <ViewportScalar as TryFrom<i32>>::Error;\n\n    fn try_from(src: (i32, i32)) -> Result<Self, Self::Error> {\n        Ok(Self {\n            x: src.0.try_into()?,\n            y: src.1.try_into()?,\n        })\n    }\n}\n\n/// The kind of view (hex, ascii, block, etc)\n#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]\npub enum ViewKind {\n    Hex(HexData),\n    Dec(HexData),\n    Text(TextData),\n    Block,\n}\n\nimpl ViewKind {\n    pub(crate) fn is_text(&self) -> bool {\n        matches!(self, Self::Text(_))\n    }\n}\n\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct TextData {\n    /// The kind of text (ascii/utf16/etc)\n    pub text_kind: TextKind,\n    pub line_spacing: u16,\n    #[serde(skip)]\n    pub edit_buf: EditBuffer,\n    pub font_size: u16,\n    /// Offset from regular ascii offsets. Useful to see custom (single byte) text encodings\n    #[serde(default)]\n    pub offset: i8,\n}\n\nimpl PartialEq for TextData {\n    fn eq(&self, other: &Self) -> bool {\n        self.text_kind == other.text_kind\n            && self.line_spacing == other.line_spacing\n            && self.font_size == other.font_size\n    }\n}\n\nimpl Eq for TextData {}\n\n#[derive(Debug, Serialize, Deserialize, Clone)]\npub struct HexData {\n    #[serde(skip)]\n    pub edit_buf: EditBuffer,\n    pub font_size: u16,\n}\n\nimpl PartialEq for HexData {\n    fn eq(&self, other: &Self) -> bool {\n        self.font_size == other.font_size\n    }\n}\n\nimpl Eq for HexData {}\n\nimpl HexData {\n    pub fn with_font_size(font_size: u16) -> Self {\n        Self {\n            edit_buf: Default::default(),\n            font_size,\n        }\n    }\n}\n\nimpl TextData {\n    pub fn with_font_info(line_spacing: u16, font_size: u16) -> Self {\n        Self {\n            text_kind: TextKind::Ascii,\n            line_spacing,\n            edit_buf: EditBuffer::default(),\n            font_size,\n            offset: 0,\n        }\n    }\n}\n\n#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]\npub enum TextKind {\n    Ascii,\n    Utf16Le,\n    Utf16Be,\n}\n\nimpl TextKind {\n    pub fn name(&self) -> &'static str {\n        match self {\n            Self::Ascii => \"ascii\",\n            Self::Utf16Le => \"utf-16 le\",\n            Self::Utf16Be => \"utf-16 be\",\n        }\n    }\n\n    pub(crate) fn bytes_needed(&self) -> u8 {\n        match self {\n            Self::Ascii => 1,\n            Self::Utf16Le => 2,\n            Self::Utf16Be => 2,\n        }\n    }\n}\n\nimpl ViewportRect {\n    fn relative_offset_of_pos(\n        &self,\n        x: ViewportScalar,\n        y: ViewportScalar,\n    ) -> Option<(ViewportScalar, ViewportScalar)> {\n        self.contains_pos(x, y).then_some((x - self.x, y - self.y))\n    }\n\n    pub fn contains_pos(&self, x: ViewportScalar, y: ViewportScalar) -> bool {\n        x >= self.x && y >= self.y && x <= self.x + self.w && y <= self.y + self.h\n    }\n}\n\n/// Try to convert mouse position to ViewportVec.\n///\n/// Log error and return zeroed vec on conversion error.\npub fn try_conv_mp_zero<T: TryInto<ViewportVec>>(src: T) -> ViewportVec\nwhere\n    T::Error: std::fmt::Display,\n{\n    match src.try_into() {\n        Ok(mp) => mp,\n        Err(e) => {\n            per!(\n                \"Mouse position conversion error: {}\\nHexerator doesn't support extremely high (>32700) mouse positions.\",\n                e\n            );\n            ViewportVec { x: 0, y: 0 }\n        }\n    }\n}\n"
  },
  {
    "path": "src/windows.rs",
    "content": "use {\n    crate::{\n        App,\n        gui::message_dialog::MessageDialog,\n        source::{Source, SourceAttributes, SourcePermissions, SourceProvider, SourceState},\n    },\n    anyhow::bail,\n    windows_sys::Win32::System::Threading::*,\n};\n\npub fn load_proc_memory(\n    app: &mut App,\n    pid: sysinfo::Pid,\n    start: usize,\n    size: usize,\n    _is_write: bool,\n    font_size: u16,\n    line_spacing: u16,\n    _msg: &mut MessageDialog,\n) -> anyhow::Result<()> {\n    let handle;\n    unsafe {\n        let access =\n            PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION;\n        handle = OpenProcess(access, 0, pid.as_u32());\n        if handle.is_null() {\n            bail!(\"Failed to open process.\");\n        }\n        load_proc_memory_inner(app, handle, start, size, font_size, line_spacing)\n    }\n}\n\nunsafe fn load_proc_memory_inner(\n    app: &mut App,\n    handle: windows_sys::Win32::Foundation::HANDLE,\n    start: usize,\n    size: usize,\n    font_size: u16,\n    line_spacing: u16,\n) -> anyhow::Result<()> {\n    unsafe { read_proc_memory(handle, &mut app.data, start, size) }?;\n    app.source = Some(Source {\n        attr: SourceAttributes {\n            permissions: SourcePermissions { write: true },\n            stream: false,\n        },\n        provider: SourceProvider::WinProc {\n            handle,\n            start,\n            size,\n        },\n        state: SourceState::default(),\n    });\n    if !app.preferences.keep_meta {\n        app.set_new_clean_meta(font_size, line_spacing);\n    }\n    app.src_args.hard_seek = Some(start);\n    app.src_args.take = Some(size);\n    Ok(())\n}\n\npub unsafe fn read_proc_memory(\n    handle: windows_sys::Win32::Foundation::HANDLE,\n    data: &mut crate::data::Data,\n    start: usize,\n    size: usize,\n) -> anyhow::Result<()> {\n    let mut n_read: usize = 0;\n    data.resize(size, 0);\n    if unsafe {\n        windows_sys::Win32::System::Diagnostics::Debug::ReadProcessMemory(\n            handle,\n            start as _,\n            data.as_mut_ptr() as *mut std::ffi::c_void,\n            size,\n            &mut n_read,\n        )\n    } == 0\n    {\n        bail!(\"Failed to load process memory. Code: {}\", unsafe {\n            windows_sys::Win32::Foundation::GetLastError()\n        });\n    }\n    Ok(())\n}\n"
  },
  {
    "path": "test_files/empty-file",
    "content": ""
  },
  {
    "path": "test_files/plaintext.txt",
    "content": "This is a plain text file used to test text rendering in Hexerator.\nIt uses ISO-8859-1 encoding to fit accented characters in a byte.\n{Hello}\nunderscored_stuff\n <- accented characters\n\nhyphen-stuff"
  }
]