Agent instructions for this repository. Layer rules are defined in ARCHITECTURE.md.
gpui-base-template — GPUI desktop app (branded Daynote) with sidebar navigation, daily notes, notes list, gpui-component UI, i18n (English/Indonesian), theme toggle, and layered feature architecture (view + action + style).
Runtime: native desktop (macOS primary target; Linux/Windows via cross-compile + bundle scripts)
Binary name: Daynote
Crate name: gpui-base-template
Data: notes and embedded images are encrypted at rest (AES-256-GCM) under the app data folder; the master key lives in the OS secure store (macOS Keychain, Windows Credential Manager, Linux Secret Service). Export decrypts in-app to readable Markdown/TXT/ZIP.
cargo build
python3 scripts/check-gpui-render-updates.py
python3 scripts/format-rust-guard-spacing.py --check
cargo runRun cargo build after every structural change. Run python3 scripts/format-rust-guard-spacing.py to fix guard/loop spacing. Run python3 scripts/check-gpui-render-updates.py when adding or changing views, routers, or shell state. Manually smoke-test language and theme toggles in Settings when touching locale or theme code.
DefaultLayout is a GPUI Entity. While it is rendering (including any notes::render, settings::render, or *_view.rs helpers called from DefaultLayout::render), calling shell.update(cx, |layout, …|) on that same entity panics:
cannot update DefaultLayout while it is already being updated
| Situation | Do this |
|---|---|
| Need mutable shell state while building UI | Precompute in DefaultLayout::render via &mut self (e.g. prepare_*() helpers), then pass &DefaultLayout into child render() |
| Need cached / derived data in a page view | Read-only getters on &DefaultLayout — never shell.update in view.rs / *_view.rs / router/mod.rs |
| Need to sync inputs or run side effects when a screen paints | window.defer(cx, …) then shell.update inside the deferred closure |
| Button / switch / async path / folder picker | shell.update in action.rs or on_* handlers is fine (not inside render) |
| Multiple mutations in one async callback | One shell.update block — never nest shell.update inside another shell.update |
CI / pre-commit guard: python3 scripts/check-gpui-render-updates.py fails if shell.update( appears under **/view.rs, **/*_view.rs, router/mod.rs, or layouts/default/view.rs. Install hooks once: ./scripts/install-githooks.sh (sets core.hooksPath=githooks; pre-commit also runs guard spacing --check).
Manual smoke after shell/view changes: navigate across all sidebar screens; open any calendar-style notes views if present. App must not abort on navigation.
See ARCHITECTURE.md (GPUI shell entity during render) for the same rules in layer terms.
For release/bundle work:
make build-release
make bundle # host OS, release
make bundle-macos # macOS .appLayering: thin router, pages under components/pages/, domain in flat src/ tree (constants, i18n, toolkits).
src/
lib.rs # i18n init, tr! macro, run(), vault migration at startup
assets.rs # App AssetSource (extends gpui-component icons)
main.rs
router/mod.rs # Thin ActiveScreen → page dispatch
layouts/default/ # App shell (DefaultLayout: action + view)
components/
pages/ # Full screens
notes/ # Notes list (search, date filter, pagination)
daily_note/ # Single-day editor + preview + copy/export
settings/ # Theme + locale
about/ # App info + privacy policy
atoms/ # page_title
molecules/
sidebar_nav_item/ # action + view + style
note_calendar/ # action + view + style
markdown_toolbar/ # action + view + style
organisms/
app_sidebar/ # action + view
app_titlebar/ # action + view
constant/ # Domain enums
app/preference_path/
nav/item_id/ active_screen/
locale/code/
note/date_key/ summary/
layout/spacing/
typography/family/ size/
i18n/ # I18n{Page} enums
about/ app/ calendar/ daily_note/ nav/ notes/ settings/
style/
tokens.rs # Theme token application
material.rs # Sidebar/titlebar/content materials, dialog surfaces
icon.rs # Shared icon sizing + custom icon paths
toolkit/ # Cross-feature adapters
locale/ theme/ daily_note/ note_vault/ notes_export/
markdown_editor/ preview_http/ date_format/ font/
assets/
icons/ # App-local SVG icons (merged via src/assets.rs)
content/
locales/app.yml # Translation copy (en, id)
scripts/ packaging/ Makefile
| Concept | Location | Role |
|---|---|---|
NavItemId |
src/constant/nav/item_id/mod.rs |
Sidebar item IDs: Daily, Notes, Settings, About |
NAV_ITEMS |
same module | Sidebar render order |
ActiveScreen |
src/constant/nav/active_screen/mod.rs |
Nav(NavItemId) or DailyNote(NoteDateKey) |
DefaultLayout |
src/layouts/default/action.rs |
Shell state: active screen, calendar, notes table, inputs |
Default screen: ActiveScreen::Nav(NavItemId::Notes)
Daily nav item: opens today's daily note (open_today_daily_note), not a standalone list page.
Router dispatch: src/router/mod.rs matches layout.active_screen() → page render(). No business logic.
Every screen lives under src/components/pages/{name}/:
src/components/pages/{name}/
mod.rs
view.rs # Markup only — calls action getters/handlers
action.rs # Getters, event handlers, enum dispatch
style.rs # Layout constants (max width, spacing, page size)
Rules:
- Router only dispatches; no page logic in
router/mod.rs. - Views do not contain handler logic or inline i18n key strings — use
action.rsandi18n/enums. - Layout state (
DefaultLayout) lives inlayouts/default/action.rs, not in page modules. - Only import component view modules (or public
render()helpers) from parents. Do not treataction.rsas a render entry point. - Pages import toolkits and constants; toolkits must not import page modules.
1. DEFAULT — imports, private enums used by handlers
2. GETTER STATE — labels, flags, resolvers via match on enums
3. HANDLER — named event functions (on_*)
4. PRIVATE — helpers extracted from handlers (same file, below handlers)
Applies to all action.rs files: pages, layouts, molecules, organisms.
These rules apply to every action.rs in the repo (components/pages/, layouts/, components/).
| Pattern | Use for |
|---|---|
| Early return on negative guard | if !condition { return; } |
Sequential single-level if |
Independent branches with early return each |
match on enums / structured destructuring |
NavItemId, ActiveScreen, ExportFormat, TableEvent |
let ... else { return ... } |
Option/Result guard without nesting |
| One loop per function | for, while, or iterator .filter().map() — not nested |
| Extract helper fn | Logic that would nest inside a loop or if |
| Pattern | Fix |
|---|---|
if/else or if ... else if chains |
Early return per branch, or match on enum |
Nested if (if inside if) |
Flatten with early return; split into helper fn |
if inside while/for body |
Move body to helper fn; loop only calls helper |
| Nested loops | Single loop + helper, or iterator pipeline |
| Nested functions | Private fns at module level in same file |
| Blank lines inside function bodies | See Spacing & vertical rhythm |
Ternary-style if a { x } else { y } in expression position |
Use .min()/.max(), match, or helper fn |
Nested if → early return:
// Bad
if is_active {
if cx.theme().mode == ThemeMode::Dark {
return cx.theme().sidebar_primary;
}
return cx.theme().sidebar_accent_foreground;
}
cx.theme().sidebar_foreground
// Good
if !is_active {
return cx.theme().sidebar_foreground;
}
if cx.theme().mode == ThemeMode::Dark {
return cx.theme().sidebar_primary;
}
cx.theme().sidebar_accent_foregroundif/else bounds → Ord methods:
// Bad
let min_date = if start <= end { start } else { end };
// Good
let min_date: NoteDateKey = start.min(end);
let max_date: NoteDateKey = start.max(end);if inside while → helper:
// Bad — if inside while
while cell_index < 35 {
week_days.push(date);
if week_days.len() == 7 {
weeks.push(week_days);
week_days = Vec::new();
}
}
// Good — loop calls helper with no conditionals
while cell_index < 35 {
push_calendar_day(&mut weeks, &mut week_days, date);
cell_index = cell_index.saturating_add(1);
}| Layer | Path pattern | action.rs |
|---|---|---|
| Atoms | src/components/atoms/{name}/view.rs |
Rare |
| Molecules | src/components/molecules/{name}/ |
When state/logic needed |
| Organisms | src/components/organisms/{name}/ |
When nav/state needed |
Molecules with logic use the same split as pages:
src/components/molecules/{name}/
mod.rs
view.rs
action.rs
style.rs # optional layout constants
Current molecules with action.rs: sidebar_nav_item, note_calendar.
Dependencies flow downward: pages → organisms → molecules → atoms → style tokens.
- Enums in
src/constant/{domain}/{name}/mod.rs— PascalCase members, one concept per folder. NavItemIdfor navigation IDs;LocaleCodefor locale codes;NoteDateKeyfor note dates.- Never duplicate enum values as string literals in views.
| Part | Location |
|---|---|
| Key registry | src/i18n/{page}/mod.rs |
| Copy | content/locales/app.yml |
| Runtime | LocaleToolkit + I18n*::translate() |
Rules:
crate::tr!("literal.key")only inside i18n registry modules (macro requires literal keys).- Views call
I18nNotes::Export.translate()or action getters — not raw keys. - Locale preference persists to
{app_data}/localeviaLocaleToolkit::apply_and_persist(). - Startup locale:
LocaleToolkit::resolve_startup_locale()(saved file → defaulten). - Privacy policy copy lives in
about.privacy_body(content/locales/app.yml); keep EN/ID in sync when storage or encryption behavior changes.
| Part | Location |
|---|---|
| App data root | preference_path::preference_dir() — ~/.daynote (Unix) or %USERPROFILE%\.daynote (Windows) |
| Note files | {app_data}/notes/{YYYY-MM-DD}.md (encrypted binary on disk, .md extension kept) |
| Images | {app_data}/images/{YYYY-MM-DD}/ (encrypted binary on disk) |
| Vault API | src/toolkit/note_vault/index.rs — NoteVaultToolkit::read_*, write_*, migrate_storage() |
| Startup migration | NoteVaultToolkit::migrate_storage() in src/lib.rs after init |
Rules:
- All note/image disk I/O goes through
NoteVaultToolkit(viaDailyNoteToolkit/NoteImageToolkit). Do notread_to_stringorwritenote/image paths directly. - File format: magic
DNT1+ nonce + AES-256-GCM ciphertext; AAD is the path relative to app data root. - Master key:
keyringwith platform features (apple-native,windows-native,sync-secret-service,crypto-rust). Do not store the key in the app data folder. - Export and preview read decrypted bytes in memory only (
NotesExportToolkit,PreviewHttpClient,DailyNoteToolkit::load). - Legacy plaintext files are re-encrypted on read or during startup migration.
- On Linux, Secret Service (GNOME Keyring / KWallet) must be available for key persistence; without it, save may fail rather than fall back to plaintext.
Each toolkit exports one PascalCase struct from src/toolkit/{name}/index.rs:
| Toolkit | Responsibility |
|---|---|
LocaleToolkit |
resolve_startup_locale(), load_saved_locale(), save_locale(), apply(), apply_and_persist() |
ThemeToolkit |
sync_system(), is_dark(), set_light(), set_dark() |
NoteVaultToolkit |
AES-256-GCM encrypt/decrypt, keychain access, legacy migration |
DailyNoteToolkit |
Load/save note content (via vault), list dates and summaries |
NoteImageToolkit |
Import/paste/cleanup images (via vault), preview URI rewrite |
NotesExportToolkit |
Export single or all notes as decrypted Markdown/TXT in ZIP |
PreviewHttpClient |
Serves local decrypted images to the markdown preview |
DateFormatToolkit |
Localized month/weekday/daily-note titles |
Features import toolkits; toolkits must not import feature modules.
Enforced by scripts/format-rust-guard-spacing.py (fix: run without --check).
| Between | Blank lines |
|---|---|
Import groups (std → external crate → crate::) |
1 |
| Imports and first top-level item | 1 |
Top-level fn, struct, enum, const, impl blocks |
1 |
impl methods |
1 |
action.rs sections (getter → handler → private helper) |
1 |
A guard is an if without else whose then-branch ends in return, or let … else { return / continue / break }.
| Rule | Blank lines |
|---|---|
| Between consecutive guards (block or one-line) | 1 |
Inside a guard block (statements before return) |
0 |
Before return inside a guard block |
0, unless the prior statement spans multiple lines |
| After the last guard | 1 only when two or more non-guard statements follow |
| After the last guard before a single fallthrough statement | 0 |
Between regular let bindings (non-guard) |
0 |
Between non-guard if blocks (side effects, no early return) |
0 |
pub fn label_color(is_active: bool, cx: &App) -> Hsla {
if !is_active {
return cx.theme().sidebar_foreground;
}
if cx.theme().mode == ThemeMode::Dark {
return cx.theme().sidebar_primary;
}
cx.theme().sidebar_accent_foreground
}pub fn on_click(item: NavItemId, shell: Entity<DefaultLayout>, …) {
if item == NavItemId::Daily {
shell.update(cx, |layout, cx| {
layout.open_today_daily_note(window, cx);
});
return;
}
shell.update(cx, |layout, cx| {
layout.select_nav_item(item, cx);
});
}| Rule | Blank lines |
|---|---|
Before while / for (after setup lets) |
1 |
After while / for (before trailing statements) |
1 |
| Inside loop body | 0 |
fn strip_markdown_links(text: &str) -> String {
let mut result: String = String::new();
let mut rest: &str = text;
let mut keep_scanning: bool = true;
while keep_scanning {
keep_scanning = process_next_markdown_link(&mut result, &mut rest);
}
result.push_str(rest);
result
}Consecutive let … else { return … } guards follow the same blank-line rules as if guards.
fn process_next_markdown_link(result: &mut String, rest: &mut &str) -> bool {
let Some(start) = rest.find('[') else {
return false;
};
result.push_str(&rest[..start]);
let Some(label_end) = after_bracket.find(']') else {
result.push('[');
result.push_str(after_bracket);
*rest = "";
return false;
};
// …
}One blank line when switching getters → handlers → private helpers. No blank line between getters in the same group.
- Explicit types on parameters, return values, and local variables where non-obvious.
- Early return on negative conditions; prefer
matchfor enum branches overif/elsechains. - No nested functions, nested loops, or nested conditionals beyond one level inside a function.
- Follow Spacing & vertical rhythm; run
scripts/format-rust-guard-spacing.pyafter editing guard or loop layout. - No unused exports, handlers, modules, or variables.
- No new external dependencies without user approval (approved vault deps:
aes-gcm,keyring,rand). - Minimize diff scope — match existing patterns in the nearest feature/module.
- Do not DRY unrelated code unless asked.
- No
console.logequivalent — use proper error handling or silent discard where existing code does.
GPUI uses programmatic styling (.px_8(), .gap_4()), not CSS modules.
| Tier | Location |
|---|---|
| Tokens | src/style/tokens.rs |
| Materials | src/style/material.rs |
| Icon sizing | src/style/icon.rs |
| Feature layout | src/components/pages/{name}/style.rs |
| Component layout | src/components/molecules/{name}/style.rs |
Use cx.theme() / ActiveTheme for colors — avoid hardcoded HSLA in features. Light-mode sidebar uses macOS-native tokens from material.rs.
- Add variant to
NavItemIdinsrc/constant/nav/item_id/mod.rsandNAV_ITEMS. - Create
src/components/pages/{name}/withview.rs,action.rs,style.rs. - Add
I18n{Name}enum insrc/i18n/{name}/mod.rsand register insrc/i18n/mod.rs. - Add keys to
content/locales/app.yml(bothenandid). - Wire dispatch in
src/router/mod.rsand register module insrc/components/pages/mod.rs. - Sidebar picks up new item automatically via
NAV_ITEMS. - If screen needs shell state, extend
DefaultLayoutinlayouts/default/action.rs.
- Put page logic in
router/mod.rs. - Register or import
action.rsas a UI render module from unrelated parents. - Duplicate domain enums or i18n keys across features.
- Use magic strings for nav items, locales, or storage paths in views.
- Read or write note/image files on disk outside
NoteVaultToolkit/ its toolkit wrappers. - Add dependencies without approval (crypto crates
aes-gcm,keyring,randare approved for vault storage). - Use nested loops, nested if, or if/else in any
action.rs. - Call
shell.updatefromview.rs,*_view.rs,router/mod.rs, orlayouts/default/view.rs(use&DefaultLayoutgetters,prepare_*on&mut self, orwindow.deferinstead). - Nest
shell.updatecalls on the same entity in one callback. - Create markdown files unless explicitly requested.
| Document | Purpose |
|---|---|
| ARCHITECTURE.md | Layer model and porting guide |
| README.md | Quick start |