This page covers the standard layout of a Nib project, how assets are discovered, and how to configure your app through pyproject.toml.
Standard layout¶
A project created with nib create follows this structure:
myapp/
├── src/
│ ├── main.py # Application entry point
│ └── assets/ # Icons, images, fonts
│ └── .gitkeep
├── pyproject.toml # Project metadata and Nib build config
└── README.md
You are free to organize your Python code however you like -- Nib only needs to know the path to your entry point script. The layout above is a convention, not a requirement.
Entry point¶
The entry point is the Python file that contains your nib.run(main) or MyApp().run() call. It is specified in two places:
- Development mode -- passed as an argument to
nib run:bash nib run src/main.py - Build mode -- configured in
pyproject.toml:toml [tool.nib] entry = "src/main.py"
Assets directory¶
The assets/ directory holds images, icons, fonts, and any other static files your app needs. Nib auto-detects the assets directory at startup by looking in these locations (in order):
assets/next to the entry point scriptassets/in the parent directory (handlessrc/main.pyfindingassets/at root)src/assets/from the working directory
For example, with the standard layout, running nib run src/main.py will find src/assets/ automatically.
Tip
You can override the auto-detected path by passing assets_dir to nib.run():
python
nib.run(main, assets_dir="my_custom_assets")
Or set it explicitly before running:
python
nib.App.set_assets_dir("/absolute/path/to/assets")
Referencing assets in code¶
Use relative paths from the assets directory when referencing files. Nib resolves them to absolute paths automatically:
# If assets/logo.png exists:
nib.Image(source="logo.png")
# Subdirectories work too:
nib.Image(source="icons/settings.png")
Absolute paths and URLs are passed through without resolution:
nib.Image(source="/Users/me/photos/cat.jpg")
nib.Image(source="https://example.com/image.png")
Fonts¶
Nib automatically detects font files placed in the assets directory. Supported formats are .ttf, .otf, .ttc, .woff, and .woff2.
Place your font files anywhere inside assets/:
src/assets/
├── Geist-Regular.ttf
├── Geist-Bold.ttf
└── icons/
└── app-icon.png
The font name is derived from the filename without the extension. Use it with Font.custom():
nib.Text(
"Hello, custom font!",
font=nib.Font.custom("Geist-Regular", size=16),
)
You can also register fonts from arbitrary paths or URLs using app.fonts:
app.fonts = {
"MyFont": "/absolute/path/to/MyFont.ttf",
"WebFont": "https://example.com/fonts/WebFont.otf",
}
Note
Fonts placed in the assets directory are registered automatically and do not need to be added to app.fonts. User-specified fonts in app.fonts take precedence if there is a name conflict.
Images¶
Place image files (.png, .jpg, .svg, etc.) in the assets directory and reference them by relative path:
# assets/icon.png
nib.Image(source="icon.png", width=64, height=64)
# assets/photos/banner.jpg
nib.Image(source="photos/banner.jpg", content_mode=nib.ContentMode.FIT)
For the app icon used in nib build, place an icon.png at src/assets/icon.png. The build system converts it to .icns format automatically.
pyproject.toml configuration¶
The [tool.nib] section configures the Nib CLI. The [tool.nib.build] section controls the nib build command.
Entry point¶
[tool.nib]
entry = "src/main.py"
This tells nib build which file to use as the application entry point.
Build configuration¶
[tool.nib.build]
# App display name (defaults to project name)
name = "My App"
# Bundle identifier (defaults to com.nib.<name>)
identifier = "com.example.myapp"
# App version (defaults to project version)
version = "1.0.0"
# App icon (defaults to src/assets/icon.png if it exists)
icon = "src/assets/icon.png"
# Minimum macOS version (defaults to current)
min_macos = "14.0"
# Packages to exclude from bundling
exclude = []
# Extra dependencies to include if auto-detection misses them
extra_deps = []
Info.plist options¶
Advanced options for the macOS application bundle:
[tool.nib.build.plist]
copyright = "Copyright 2025 Your Name"
category = "public.app-category.utilities"
notification_style = "banner" # "banner", "alert", or "none"
dock_icon = false # true to show in Dock
url_schemes = ["myapp"] # Custom URL schemes
# Privacy usage descriptions
[tool.nib.build.plist.usage]
camera = "This app needs camera access for video capture."
microphone = "This app needs microphone access for recording."
Development mode vs bundled mode¶
Nib apps behave differently depending on how they are launched:
Development mode (nib run)¶
- Python launches the Swift runtime as a child process.
- Assets are read directly from the filesystem using the auto-detected path.
- File changes trigger hot reload -- the Python process restarts while the Swift runtime stays alive.
Bundled mode (nib build output)¶
- The Swift runtime is the main executable. It launches an embedded Python interpreter.
- Assets are bundled inside the
.appatContents/Resources/assets/. - The
NIB_SOCKETenvironment variable is set by the Swift runtime so Python knows it is running in bundled mode.
Warning
Paths that work in development (e.g., ../../data/file.txt) may not work in a bundled app. Always use the assets directory for static files, or resolve paths at runtime using os.path relative to your script.
Asset resolution is handled transparently. Code like nib.Image(source="logo.png") works in both modes without changes -- Nib resolves the path from the assets directory regardless of whether it is on disk or inside the app bundle.