A game modding toolkit for Dispel RPG. Reads/writes game files in native binary, INI, and DB formats. All edits are persisted directly — no intermediate database. Includes a full desktop GUI for editing game data with undo/redo, mod packaging, and a hex editor.
This workspace is a 5-crate Cargo workspace (Rust 2021, edition 2021) versioned 0.7.1:
| Crate | Type | Role |
|---|---|---|
dispel-extractor (root) |
lib + bin | dispel-core library + CLI binary. Game logic only. |
dispel-gui |
bin only | Iced 0.15 desktop GUI. No lib.rs — pure binary crate. |
dispel-macros |
proc-macro | 5 derive macros used by dispel-core. |
gui-widgets |
lib | Reusable custom Iced widgets (ContextMenu, modal, ParagraphCache). |
hexedit |
lib + bin | Full hex editor with Lua scripting — used by dispel-gui as a tab type. |
Separation of concerns:
dispel-core+dispel-macros: game logic only. Zero GUI/presentation code.dispel-gui+gui-widgets+hexedit: UI only. Zero game logic (they consumedispel-core).dispel-extractor(rootmain.rs): thin CLI wrapper around the library.
dispel-extractor/
├── src/ # dispel-core library
│ ├── lib.rs # Public re-exports
│ ├── main.rs # CLI binary entry
│ ├── cli.rs # clap subcommand definitions
│ ├── database.rs # SQLite schema bootstrap
│ ├── localization.rs # Text export/import, Localizable trait
│ ├── sprite.rs # .spr parser
│ ├── snf.rs # .snf audio parser
│ ├── references/ # 30 game data file parsers
│ ├── map/ # .map parser + isometric renderer
│ ├── modding/ # Mod authoring pipeline
│ ├── commands/ # CLI command implementations
│ └── queries/ # 70 SQL files (schema DDL + INSERTs)
├── dispel-macros/ # proc-macro crate
│ └── src/ # 5 derive macros
├── gui-widgets/ # Iced widgets library
│ └── src/components/ # ContextMenu, modal, ParagraphCache
├── hexedit/ # Hex editor library + standalone bin
│ └── src/ # 20+ modules incl. Lua scripting
├── dispel-gui/ # Iced 0.15 desktop GUI
│ └── src/
│ ├── app.rs # App (transient UI state)
│ ├── state.rs # AppState (persistent model)
│ ├── workspace.rs # Tabs, EditorType enum (52 variants)
│ ├── editor_registry.rs # All 35+ editor states
│ ├── platform.rs # OS-specific ops
│ ├── auto_save.rs # DraftManager (persists to ~/.config)
│ ├── style.rs # Iced style functions (~1430 lines, "Medieval" theme)
│ ├── editors/ # 38 editor subdirectories
│ ├── components/ # Reusable UI components
│ ├── message/ # Message enum hierarchy + macros
│ ├── update/ # Message handlers (domain logic)
│ ├── view/ # Pure render functions
│ ├── workspace/ # Workspace tests
│ ├── indexation/ # File/asset caching + full-text search
│ └── recording_tests.rs # Mod recording integration tests
├── tests/ # Integration tests (round_trip.rs, etc.)
├── docs/ # 4 overview .md + docs/files/ (32 per-format .md)
├── fixtures/ # Test game files (Dispel/, save data, etc.)
├── scripts/release.sh # Automated release script
├── Makefile # Dev/extract targets
└── .github/workflows/ # CI: test.yml (build only), release.yml (5 platforms)
| Ext | Category | Encoding | Purpose |
|---|---|---|---|
.db |
Database | Binary + text | Items, monsters, magic, stats |
.ini |
Config | EUC-KR or 1250 | Maps, NPCs, monsters, visuals |
.ref |
Placement | 1250 | Entity instances on maps |
.dlg / .pgp |
Dialogue | EUC-KR / 1250 | Scripts + text |
.scr |
Script | 1250 | Quests, messages |
.map |
Geometry | Binary | Tiles, sprites, events |
.gtl / .btl |
Tilesets | RGB 565 | Ground/roof tiles (32×32) |
.spr |
Sprites | RGB 565 | Character animations |
.snf |
Audio | PCM | Sound effects |
Encoding is critical: Event.ini, Npc.ini, Monster.db (desc) → EUC-KR. Monster.ini, Store.db, WeaponItem.db → WINDOWS-1250. Per-file encoding documented in docs/files/CROSS_REFERENCES.md.
| Value | Type | Database |
|---|---|---|
| 1 | Weapon | WeaponItem.db |
| 2 | Heal | HealItem.db |
| 3 | Edit | EditItem.db |
| 4 | Event | EventItem.db |
| 5 | Misc | MiscItem.db |
- AllMap.ini → Map.ini (per-map config) → Map.map (geometry, sprites, events)
- Monster.ini (visual) + Monster.db (combat stats)
- Npc.ini (visual) + DlgMapFiles.dlg (dialogue) + PgpMapFiles.pgp (text)
- See
docs/files/CROSS_REFERENCES.mdfor the full 321-row cross-reference table and 9 relationship diagrams.
Binary DB records (in src/references/):
WeaponItem, HealItem, EditItem, EventItem, MiscItem, Monster, MagicSpell, Store, ChData, PartyIniNpc, PartyLevelNpc (+ PartyLevelRecord)
Binary ref records:
MonsterRef (Mondun*.ref), NPC (Npccat*.ref, 672 bytes), ExtraRef (Extdun*.ref)
INI/CSV/Text records:
Map, MapIni, Event, Extra, MonsterIni, NpcIni, WaveIni, Message, Quest, EventNpcRef, PartyRef, DrawItem, EventScript, DialogueScript, DialogueParagraph
- All integers: little-endian
- Colors: RGB 565 (16-bit). Helpers:
sprite::Color::from_rgb565()/to_rgb565() - Map tiles: seeked from EOF (event/GTL/BTL layers)
- Sprites: start at byte 268; validation:
ints[11] * ints[12] == ints[13] - Tilesets: contiguous 32×32 tiles, RGB 565 (2 bytes/pixel)
- Map rendering: isometric projection, 62×32 display size. Warning: full map ≈ 300MB; GUI uses viewport + LRU cache (≤50MB via
lru0.12). - SNF audio: custom PCM header; prepend RIFF WAVE header for conversion (
SnfFile::to_wav_bytes()) - DB record sizes (all fixed): WeaponItem 284B, Monster 160B, Magic 88B, HealItem 252B, EditItem 268B, EventItem 240B, MiscItem 256B, ChData 84B, Store 948B (variable product list), PartyIniNpc 28B, PartyLevelNpc 5760B (8×20 grid), NPC 672B, MonsterRef 56B, ExtraRef 184B.
See docs/file_formats.md and per-file docs/files/*.md for detailed specs.
| Trait | File | Purpose |
|---|---|---|
Extractor |
references/extractor.rs |
parse(reader, len) -> Vec<Self>, to_writer(), read_file, save_file. All 28+ game data structs implement this. |
Localizable |
localization.rs |
Extract/apply translatable text fields. Auto-derived via #[derive(Localizable)]. |
RecordPatcher |
modding/patcher.rs |
Applies mod patches to individual records. Two derive variants (binary + text). |
Command |
commands/mod.rs |
CLI command trait: fn execute(&self) -> Result<(), Box<dyn Error>> |
14 files: apply, bsdiff, change, changelog, conflicts, manifest, package, patcher (+ per-type), registry, resolution, value, vanilla, workspace.
ModdingError(thiserror, inmodding/error.rs):Io,Zip,Json,Malformed(String),MissingEntry(String),UnsupportedManifestVersion(u32)- Other modules use
std::io::Result/rusqlite::Resultdirectly.
read_null_terminated_windows_1250(bytes)— inreferences/extractor.rsTextEncodingenum (Windows1250,EucKr,Utf8) +encoding_rs_for(),encoded_len()inlocalization.rsWINDOWS_1250/EUC_KRused directly fromencoding_rscrate
70 SQL files: 37 create_table_*.sql (schema DDL) + 33 parameterized insert_*.sql. Run by database.rs to drop and recreate the schema.
55+ #[cfg(test)] blocks across the crate. Every parser has parse_single_* / parse_two_* / serialize_round_trip tests.
5 derive macros using syn 2 + quote 1:
| Macro | Generates | Use for |
|---|---|---|
#[derive(Extractor)] |
Extractor impl (binary) |
Binary .db / .ref structs |
#[derive(TextExtractor)] |
Extractor impl (text) |
INI / CSV / .scr structs |
#[derive(RecordPatcher)] |
RecordPatcher impl (binary) + unit struct |
Modding for binary formats |
#[derive(TextRecordPatcher)] |
RecordPatcher impl (text) + unit struct |
Modding for text formats |
#[derive(Localizable)] |
Localizable impl |
Translation extraction |
Attributes: #[extractor(id, string, primitive, enum_from_*, padding, array, skip, ...)] on binary; #[extractor(field=N, parse_null, enum_from_i32)] + struct-level encoding, delimiter, comment_char on text; #[patcher(filename=...)] or #[patcher(extension=..., stem_prefix=...)]; #[translatable(encoding=..., max_bytes=N)].
Used by 26+ structs in src/references/. No direct unit tests — verified indirectly via round-trip tests.
| Widget | Location | Purpose |
|---|---|---|
ContextMenu<M> |
components/context_menu/ |
Right-click overlay; uses native AppKit/Win32 menus where possible (mac/Windows) |
modal() + Modal<M,T,R> |
components/modal.rs |
Backdrop overlay with escape/click-outside dismiss, 3 unit tests |
ParagraphCache |
components/paragraph_cache.rs |
LRU-cached Iced Paragraph (16K entries, ~16MB), 5 unit tests |
style::* |
style.rs |
6 stylesheet functions: context_menu, menu_item, menu_separator, menu_disabled_item/text (dark RPG leather theme) |
Consumers: dispel-gui (file tree, tab bar, spreadsheet, editor modals) and hexedit (hex matrix, inspector, modals).
Standalone library + 2 binaries (hexedit-bin, bin/hexedit). 20+ source files. Iced 0.15 with optional lua feature (default on, Lua 5.4 vendored via mlua).
20+ files in state, config, message, update, view/*, provider, selection, editing, inspector, pattern, search, goto, coloring, vanilla_diff, lua_engine. 11 have unit tests.
Used by dispel-gui: stored as HashMap<usize, HexEditorState> in EditorRegistry, rendered in tabs, with save wired to mod packager via OnSaveFn.
- UI: Iced 0.15 (GPU via wgpu, Elm/MVU, advanced + lazy features)
- Async: Tokio multi-thread runtime +
iced::Task::perform - Core:
dispel_core(sibling crate) - Search: nucleo-matcher 0.3 (fuzzy full-text)
- SQLite: rusqlite 0.39 (bundled, for
DbVieweronly) - Audio: rodio 0.21 (SNF playback)
- File dialogs: rfd 0.17.2
- Window: 1100×800, custom "Medieval" theme (dark leather/gold)
- macOS native: objc2 + objc2-app-kit + objc2-foundation (context menus, file manager reveal)
- Accessibility: Iced 0.15
accessibilityfeature with vendoredaccesskit 0.24(../../iced/vendor/accesskit); enabled ondispel-guionly. The workspace[patch.crates-io]pinswinitto the same iced-rs fork for version alignment. Onlytable_widgetimplements full accesskit nodes (accessibility()+accessibility_action()). Custom widgets (context menu, modal, hex matrix) use the default no-op methods. - Tests:
iced_test0.14 (optional feature),proptest,syn
user action → Message → update/ handler (mutates App + AppState) → view/ (pure render)
Lives on App (transient UI) |
Lives on AppState (persistent model) |
|---|---|
file_tree (UI tree) |
editors: EditorRegistry (all 35+ editor states) |
window_id, app_mode |
workspace: Workspace (tabs, game path, recent) |
search_index (full-text) |
status_msg, shared_game_path |
draft_manager |
is_running, recording: Option<RecordingSession> |
command_palette, error_dialog |
pane_state (sidebar/main/history) |
is_indexing |
lookups: HashMap<String, Vec<(String, String)>> |
global_search, file_index_cache_manager |
|
recent_files (last 10) |
Rule of thumb: Transient UI state → App. Persistent game/model state → AppState.
Top-level Message enum (message/mod.rs:21) has 6 variants:
| Variant | Routes to | Contents |
|---|---|---|
Workspace(InternalWorkspaceMessage) |
update/workspace.rs |
Tab bar, sidebar, command palette, global search, pane grid, tool tabs |
Editor(EditorMessage) |
update/editor/mod.rs |
35+ per-editor messages |
FileTree(FileTreeMessage) |
update/file_tree.rs |
File tree actions |
Viewer(ViewerMessage) |
(DbViewer handler) | SQLite DB viewer queries, pagination, CSV export |
System(SystemMessage) |
update/system.rs |
Undo, Redo, Save, index, drafts, errors, file scan |
StartPage(StartPageMessage) |
update/startpage.rs |
Game path selection |
MessageExt trait (message/ext.rs, generated by define_message_ext! macro) provides shorthand constructors like Message::weapon(WeaponEditorMessage::LoadCatalog).
The EditorType enum (workspace.rs:9) has 52 named variants + Unknown (with #[serde(other)]).
Standard editors (8) — macro-generated via define_standard_editor!:
weapon, monster, heal_item, misc_item, edit_item, event_item, party_level_db_level, party_ref
Custom boxed editors (17) — custom state in Box<...> (all listed in EditorRegistry):
monster_ini, npc_ini, magic, store, party_ini, all_map_ini, draw_item, event_ini, event_npc_ref, extra_ini, map_ini, message_scr, quest_scr, event_scr, wave_ini, chdata, party_level_db
Tabbed editors (5) — TabbedEditor<T> (HashMap<tab_id, MultiFileEditorState>):
monster_ref, dialogue_script, dialogue_paragraph, extra_ref, npc_ref
Single-instance special editors (4):
viewer (DbViewer), chest_editor (filtered ExtraRef), mod_packager_editor (ModPackagerState), localization_manager
Per-tab HashMap editors (5) — one state per open tab:
sprite_viewers, map_editors, tileset_editors, snf_editors, hex_editors
Generates from a single declaration:
define_standard_editor! {
name: weapon,
name_pascal: Weapon,
record: dispel_core::WeaponItem,
field: weapon_editor,
file: "CharacterInGame/weaponItem.db",
}Produces: pub type WeaponEditorState = StandardEditor<WeaponItem>, pub type WeaponEditorMessage = StandardEditorMessage<WeaponItem>, pub fn handle(msg, app) -> Task<Message>, pub fn view(app) -> Element<Message>.
Key files: components/standard/macros.rs, components/standard/state.rs, components/standard/update.rs, components/standard/message.rs.
Aggregates all editor state fields. Provides consolidated lifecycle:
| Method | Role |
|---|---|
remove_tab(tab_id) |
Cleans up per-tab editors (map, tileset, hex, SNF, sprite, tabbed) on tab close |
close_all_tabs() |
Resets only HashMap-based editors (preserves single-instance Box editors) |
clear_all() |
Resets every editor to default (workspace change) |
undo_active(et, tab_id, lookups) |
Delegates to EditHistory::undo for active editor (uses undo_redo_dispatch! macro) |
redo_active(et, tab_id, lookups) |
Delegates to EditHistory::redo |
refresh_spreadsheet(et, tab_id, lookups) |
Recomputes spreadsheet caches after undo/redo |
get_active_edit_history(et, tab_id) |
Returns Option<&EditHistory> for the active editor |
stop_snf_playback() |
Stops any playing SNF audio |
Principle: AppState exposes only pass-through methods — all real work lives in EditorRegistry.
pub struct Workspace {
pub tabs: Vec<WorkspaceTab>, // Dynamic tab list
pub active_tab: Option<usize>,
pub next_id: usize, // Monotonic tab ID counter
pub game_path: Option<PathBuf>,
pub recent_files: Vec<PathBuf>,
pub last_reindexed_at: Option<u64>,
pub recent_game_paths: Vec<PathBuf>, // Max 5
}WorkspaceTab { id, label, path, editor_type, modified, pinned }. Methods: open(), open_with_editor_type(), open_tool(), close(), mark_modified(), clear_all_tabs(), save()/load() (JSON to ~/.config/dispel-gui/workspace.json), validate_timestamp(), debug_info(). EditorType::from_path() auto-detects editor from file extension.
3-pane layout (sidebar | main content | history panel) using pane_grid::State<PaneContent>. PaneContent enum: Sidebar | MainContent | HistoryPanel. PaneState tracks focus and maximized.
- Keyboard listeners: Ctrl+Z/Y/S/H/P/F/W, Shift+X
- Sprite animation tick (16ms)
- SNF playback poll (250ms)
- Event script indexing poll (100ms)
- Spreadsheet navigation (arrow keys)
| Function | Location | Renders |
|---|---|---|
App::view() |
view/mod.rs |
Top-level dispatch: start page or editor view |
App::view_start_page() |
view/start_page.rs |
Game path selection card with recent paths |
App::view_editor() |
view/mod.rs |
Full pane grid (sidebar + tab bar + editor + history) |
App::view_sidebar() |
view/mod.rs |
File tree + tools section |
App::view_recent_files() |
view/mod.rs |
Recent files list (10 most recent) |
view_history_panel() |
view/history_panel.rs |
Undo/redo stack display |
Per-editor view() |
editors/*/view.rs |
Editor-specific content (38 editors) |
RecordingSession lives on AppState. Every FieldChanged handler calls observe_field_change() in editors/mod_packager/recording/. Records are debounced via PendingEdit / RecordingKey per field edit, then flushed to the mod changelog on save. 12 integration tests in recording_tests.rs cover macro-generated, custom-wrapper, fully-custom, and tab-based editor types.
DraftManager (auto_save.rs) persists in-progress edits to ~/.config/dispel-gui/drafts.json. Detects conflicts on app start, allows user to apply or discard. SystemMessage::Draft* variants drive the flow.
indexation/ module:
file_index_cache.rs—FileIndexCache+FileIndexCacheManager(persistent bincode on disk)indexation_service.rs—IndexationServicebackground file scanningsearch_index.rs—SearchIndexnucleo-matcher full-text search with persistencetests.rs— Indexation tests
- Implement
Extractor(derive viadispel-macros) indispel-core/src/references/ - If single-file spreadsheet: use
define_standard_editor!macro indispel-gui/src/components/standard/macros.rsdeclaration - Otherwise: create
editors/my_editor/{mod,state,message,update,view}.rs - Add field to
EditorRegistry(notAppState— editors are consolidated) - Add
EditorMessagevariant inmessage/editor/mod.rs(usedefine_message_ext!macro for shorthand) - Add handler in
update/editor/and route viaupdate/editor/mod.rs - Wire
EditorType::from_path()for file extension detection (inworkspace.rs) - Wire
view/mod.rsdispatch (theview_editormatch) - Wire
EditorRegistrylifecycle methods (remove_tab,undo_active, etc.) - Delete old editor code — never keep both
| Term | Meaning |
|---|---|
catalog / catan |
The in-memory Vec<R> of all loaded records — both spellings appear (legacy typo) |
filtered |
Subset of catalog matching current search query |
edit_buffers |
Per-field String buffers for in-progress edits (not committed to catalog) |
GenericEditorState<R> |
Core reusable editor: catalog, selection, edit_history |
StandardEditor<T> |
GenericEditorState + SpreadsheetState — used by 8 standard editors |
TabbedEditor<T> |
HashMap<tab_id, MultiFileEditorState> + HashMap<tab_id, SpreadsheetState> |
MultiFileEditorState<R> |
Like GenericEditorState but with active-file tracking for multi-file formats |
- Async: Use
iced::Task+ Tokio. Never block the UI thread. - State mutation:
update/only. Views are pure. - Views: defined as
impl Appblocks inview/*.rs, notapp.rs. - Messages: use
define_message_ext!macro for shorthand constructors. - Scan vs Browse: "Scan" = load from
shared_game_path; "Browse" = file picker. - SQLite: only
DbViewer. Other editors read/write game files directly. - Enums over booleans: e.g.
LoadingState<T> { Idle, Loading, Loaded(T), Failed(String) } - Editor state lives on
EditorRegistry, notAppState. - Iced 0.15:
container(...).center_x(width)is a convenience setter forcontainer(...).width(width).align_x(alignment::Horizontal::Center).
| Path | Contents |
|---|---|
docs/overview.md |
High-level project intro |
docs/file_formats.md |
Binary format specs for SNF, map, tileset, sprite |
docs/rendering.md |
Isometric map rendering: coordinate transforms, layer ordering, painter's algorithm, sprite transparency |
docs/database_and_references.md |
SQLite schema overview |
docs/files/*.md |
32 per-format docs (one per game file type) + CROSS_REFERENCES.md (321-row cross-reference table, 9 dependency diagrams) |
Always read the relevant docs/files/*.md before modifying a parser.
fmt,cargo_test,clippy,run,help— basic deviced_test—cargo test -p dispel-gui --features "iced_test app::tests"(equivalent:rtk cargo test -p dispel-gui --features "iced_test app::tests")sound,sprite-sprite,sprite-animation,map-render,map-atlas-gtl/btl— extraction/render testsextract-*(18 targets) — extract individual game files to JSONdatabase-import— SQLite import
.github/workflows/test.yml— CI on push tomaster:cargo buildon ubuntu-latest (tests NOT run in CI).github/workflows/release.yml— Release on tagv*.*.*: 5 builds (CLI Win/Linux, GUI Win/Linux/macOS ARM), archives with SHA256, uploads to GitHub release (draft)
scripts/release.sh automates: validate semver, master branch, clean tree → cargo fmt --all --check + cargo test --workspace --all-features (script uses bare commands internally) → bump version in all workspace Cargo.toml → regen Cargo.lock → commit + tag v{major}.{minor}.{patch} → print push instructions.
dispel-core:thiserrorfor enumerable error types (seeModdingError)- GUI/CLI:
anyhowfor contextual bubbling - Never
.unwrap()on file I/O — show errors instatus_msg - Never swallow I/O or serialization errors with
let _ =or.unwrap_or_default()— log them viaeprintln!or propagate with?
- Use enums over booleans:
LoadingState<T> { Idle, Loading, Loaded(T), Failed(String) } - Always use
Task::performfor async work — never block
- Clippy: zero warnings (
rtk cargo clippy --workspace --all-features --test -- -D warnings) - Compiler warnings: use
rtk cargo test --workspace --all-features --no-runto compile all crates (including tests) and surface any compiler warnings without waiting for tests to execute — useful as a fast pre-commit check - Format:
cargo fmt --allbefore commit - Validate all binary bounds before indexing
Writing effective tests: The goal is finding real bugs, not just confirming compilation. Prefer integration tests (iced_test) that simulate real user flows — they catch behavioral regressions, state corruption, and edge cases that unit tests miss.
- dispel-core: unit test every new parser with hardcoded byte slices
- dispel-gui: write iced_test integration tests that simulate real user flows (open tabs, edit data, undo/redo, save). Test state transitions and behaviors, not visuals.
- Round-trip tests (
tests/round_trip.rs): read fixture → parse → write → verify byte-for-byte match - Integration tests (
tests/integration_weapon_item.rs): end-to-end load → edit → save workflow - Test naming:
test_${scenario}_${condition}e.g.test_undo_weapon_editor_empty_history - Test helpers: tests in
$module/tests.rs(co-located), separaterecording_tests.rsfor recording - Property tests: use
proptestfor invariants - Run before every commit:
rtk cargo test --workspace --all-features
cargo check --message-format=short: fast compile errors- Always use
rtkprefix for cargo/git operations (see rtk section above for the full list) ripgrep/rg: fast code searchfd: faster thanfindrust-analyzer: essential LSP
# Extract INI/DB/reference files to JSON
cargo run -- extract -i "AllMap.ini"
cargo run -- extract -i "Monster.db"
# Sprites
cargo run -- sprite "file.spr" output_name
# Maps
cargo run -- map tiles "file.gtl" --output dir/
cargo run -- map render --map file.map --btl file.btl --gtl file.gtl --output out.png
# Audio
cargo run -- sound "file.snf" output.wav
# SQLite (optional)
cargo run -- database import "path/to/Dispel/" db.sqlite
# Modding (see src/commands/modding or src/modding/ for full surface)
cargo run -- mod-pack ...Subcommands defined in src/cli.rs: extract, patch, validate, list, schema, sprite, sound, dialog, map, database, mod-pack, test.
✅ Permitted:
- Analyzing file formats, documenting specs
- Creating modding/interoperability tools
- Using "Dispel" for identification only
❌ Prohibited:
- Extracting/distributing copyrighted content
- Bypassing copy protection
- Commercial exploitation
- Using DISPEL® trademark beyond identification
When mentioning Dispel: Use DISPEL® with ® symbol on first mention. Include disclaimer: "not affiliated with, endorsed by, or sponsored by the trademark owner."
- Circular imports: GUI ↔ core must never share presentation code
- Blocking UI: All file I/O is async via Task
- Unsafe parsing: Validate all bounds before indexing
- Hardcoded paths: Use
dirscrate for config/cache (~/.config/dispel-gui/) - Text encoding: Check encoding table before reading — wrong codec = corruption
- Map memory: Never load full rendered map (~300MB); use viewport + LRU cache
- Macros have no unit tests:
dispel-macrosis verified only via dependent crate's tests - Editor count drifts: Verify editor count against
EditorRegistryandEditorTypeif unsure
cargo build --workspace # Build all
cargo test --workspace --all-features --quiet # Test all
cargo test --workspace --all-features --no-run # Compile all crates including tests (catches compiler warnings fast)
cargo clippy --workspace -- -D warnings # Lint
cargo fmt --all # Format
cargo check -p dispel-gui --message-format=short # Fast GUI errors
cargo run -p dispel-gui # Launch GUI
make iced_test # Iced UI simulation testsAlways use rtk instead of the bare command when running these operations. It compresses output by 60-90%, saving significant context window for real work.
Note: the rtk hook only applies to Bash tool calls — built-in tools like Read, Grep, and Glob bypass it. For token-efficient file access, prefer shell commands through rtk read, rtk grep, rtk ls instead of the built-in tools.
rtk cargo test -v [cargo test args...] # Tests with compact output (failures only, ~-90%)
rtk cargo build # Build output filtered (~-80%)
rtk cargo clippy # Clippy warnings grouped (~-80%)
rtk git status # Compact status
rtk git diff # Condensed diff
rtk git log -n 10 # One-line commits
rtk ls . # Token-optimized directory tree
rtk grep "pattern" . # Grouped search results
rtk read file.rs # Smart file readingThis project used the iced from the ../iced. It is a nightly version (v0.15-dev) with modifications to handle accesiblity features (like a VoiceOver screen reader).
Custom patterns are in the gui-widgets crate - ./gui-widgets/src.
There are min and max methods to the Length type, allowing to define minimum and maximum constraints for any widget:
row![
container(sidebar).width(Fill.min(150).max(200)),
container(content).width(FillPortion(3).min(300)),
]window::Event::Resized is notified when scale_factor changes.
Last updated: 2026-06-04
DISPEL® is a registered trademark. This project is not affiliated with, endorsed by, or sponsored by the trademark owner.