Concepts#
This page covers the architecture and conventions behind a VIStk project. For a hands-on walkthrough, start with Quickstart.
Project Structure#
A VIStk project has the following folder layout:
MyProject/
├── .VIS/
│ ├── project.json <- project registry (screens, versions, metadata)
│ ├── settings.json <- application settings (0.6.0)
│ ├── Host.py <- Host entry point (generated by VIS new; not user-editable)
│ ├── Templates/ <- screen and element templates used by the CLI
│ └── project.spec <- PyInstaller spec file (generated on release)
├── Screens/
│ ├── defaults.py <- shared imports for all screens and elements
│ ├── root.py <- standalone Tk root used by the __main__ guard
│ └── <screen>/ <- UI element files, prefixed f_ (e.g. f_header.py)
├── modules/
│ └── <screen>/ <- logic files, prefixed m_ (e.g. m_header.py)
├── Icons/ <- .ico (Windows) or .xbm (Linux) icon files
├── Images/ <- image assets used by VIMG
├── OIM/ <- (optional) Outside Installable Media
│ └── <name>/ <- non-VIStk media shipped with the installer
├── <screen>.py <- main script for each screen
└── dist/ <- compiled binaries (created on release)
project.json is the source of truth for the project. It stores screen names, script
paths, icons, descriptions, version numbers, and release configuration. It is managed
automatically by the CLI and by VINFO/Project — do not edit it by hand.
settings.json holds per-project application settings — window, appearance, Host
and notification preferences. Unlike project.json it is meant to be edited, either
through the built-in Settings window or by hand. See Application Settings below.
OIM/ is an optional folder convention (nothing is registered in project.json) for
Outside Installable Media: installable payloads the VIStk build pipeline cannot
produce itself — a COM-registered add-in, an MSI, a driver — shipped inside the same
installer and offered as an opt-in install choice. Each OIM/<name>/ holds a
media/ payload, an optional manifest.json and icon/, and
script/install.py / script/uninstall.py run by the installer and uninstaller.
Release.clean() copies the folder to the install root of the build.
App Lifecycle#
VIStk supports two runtime models: standalone (original) and Host-based (tabbed).
Standalone mode#
Any screen script can be run directly as its own Python process (python Home.py) via
the __main__ guard at the bottom of the file. This is the original VIStk behaviour and
remains the fastest way to develop one screen in isolation.
There is no in-process screen switching in this mode: calling Project().open() with no
Host running falls through to Screen.load(), which spawns a Host subprocess for the
target screen. In a compiled build nothing is spawned — the shipped executable is the
Host.
from VIStk.Objects import Root
root = Root()
root.screenTitle("MyScreen")
root.WindowGeometry.setGeometry(width=800, height=600, align="center")
# ... build your UI ...
if __name__ == "__main__":
while root.Active:
root.update()
Host mode#
The Host owns a hidden Tk() root and is never itself a visible window. All visible
windows are DetachedWindow instances (Toplevels) that the Host manages. Screens marked
tabbed: true open as tabs inside the active window; standalone screens open as new
chromeless DetachedWindow instances.
The Host is not a background service and there is no system tray. It lives exactly as
long as its windows: it starts with the first one, and once the last DetachedWindow
closes it tears down the root, releases the single-instance lock, and the driver loop
exits. Closing every window ends the process. While a Host is alive a localhost socket
provides single-instance forwarding, so a second VIS MyApp launch hands its request to
the running Host instead of starting a second one.
On the first call to host.update(), the Host automatically opens the project’s default
screen (set in project.json). This deferred open ensures that Host.py has time to
configure shared menus before any window is created.
from VIStk.Objects import Host
from modules.menu import shared_menu_structure
host = Host()
host.default_menu_setup = lambda m: m.build_shared_menu(shared_menu_structure())
while host.Active:
host.tick_fps()
host.update()
Screen navigation from anywhere in the app:
# Routes through the Host if one is running, otherwise spawns one
from VIStk.Structures._Project import Project
Project().open("WorkOrders")
Key rules:
Do not call
root.mainloop()— this bypasses thewhileloop and prevents the Host from driving screenloop()hooks.Do not call
root.destroy()to quit — callhost.quit_host()instead.Screen scripts must include
if __name__ == "__main__":around the startup code so they can be imported as modules by the Host without executing the top-level loop.Use
Project.open()instead ofProject.load()when a Host may be running — it routes correctly in both modes.
Screen Module Pattern#
Every tabbed screen must follow this pattern. Violating it will crash the Host on import:
# Module-level: only imports and pure data (no widget creation, no Tk calls)
from Screens.defaults import *
def loop(): pass # called every tick
def configure_menu(menubar): pass # contribute to HostMenu when tab is active
def on_focused(): pass # tab gained focus
def on_unfocused(): pass # tab lost focus
def on_quit() -> bool: pass # return False to veto closing the tab
def setup(parent):
# ALL widget creation goes here
pane = LayoutFrame(parent)
pane.place(relx=0, rely=0, relwidth=1, relheight=1)
if __name__ == "__main__":
from Screens.root import root, frame
setup(frame)
while root.Active:
root.update()
The if __name__ == "__main__": guard is required for tabbed screens. When the Host
imports the module to call setup(), the guard prevents the standalone loop from
running.
Hook lookup: Under the per-tab namespace design each open tab gets its own copy of the
screen’s modules, and every hook (on_focused, on_unfocused, on_quit,
configure_menu, has_unsaved) is read off that namespace. Define them in the screen
script; a hook defined only in modules/<screen>/m_<screen>.py is not consulted.
The f_element Pattern#
Elements are modular UI sections within a screen. Each element has a build(parent)
function that receives the parent frame and places widgets into it.
# Screens/MyScreen/f_header.py
widget_ref = None # module global -- set by build(), read by loop()
def build(parent):
global f_elem, widget_ref
f_elem = ttk.Frame(parent)
f_elem.place(parent.Layout.cell(row, col)) # 1-indexed
widget_ref = ttk.Label(f_elem, text="Header")
widget_ref.pack()
The #% Marker System#
VIStk templates use #% comment markers as structural anchors. The stitch command
uses these to locate and rewrite import blocks in screen scripts.
Warning
Do not delete or rename #% comment lines. They are not standard comments — they are
structural anchors that VIStk searches for by text pattern.
The critical blocks:
#%Screen Elements
import Screens.MyScreen.f_header
import Screens.MyScreen.f_body
#%Screen Modules
import modules.MyScreen.m_header
import modules.MyScreen.m_body
Inside setup():
#%Build Screen Elements
Screens.MyScreen.f_header.build(pane)
Screens.MyScreen.f_body.build(pane)
Inside loop():
#%Predefined Loop Functions
if bool(getattr(modules.MyScreen.m_header, "_m_header", False)): modules.MyScreen.m_header._m_header()
if bool(getattr(modules.MyScreen.m_body, "_m_body", False)): modules.MyScreen.m_body._m_body()
#%User Defined Loop Functions
pass
stitch replaces the content under #%Screen Elements with fully-qualified imports
for each Screens/<screen>/f_*.py file. #%Screen Modules is replaced with
fully-qualified imports for each modules/<screen>/m_*.py file. #%Build Screen
Elements is replaced with build(pane) calls, and #%Predefined Loop Functions is
replaced with guarded callbacks for each module.
Import conventions:
Screens.*andmodules.*are always imported as full dotted paths (import Screens.MyScreen.f_header), never withfromoras.fromimports inside functions are only used in the__main__guard (from Screens.root import root, frame).
project.json#
project.json is managed by the CLI and VINFO. Its structure:
{
"MyApp": {
"Screens": {
"Home": {
"script": "Home.py",
"release": true,
"icon": "home",
"window_icon": null,
"desc": "Main dashboard",
"docs": "https://example.com/docs/home",
"tabbed": true,
"single_instance": false,
"version": "1.0.0",
"current": null
}
},
"defaults": {
"icon": "app",
"window_icon": "app-titlebar",
"default_screen": "Home"
},
"metadata": {
"company": "My Company",
"copyright": "My Company",
"version": "0.1.0"
},
"release_info": {
"location": "./dist/",
"hidden_imports": [],
"compiler": "msvc"
},
"host": {
"script": ".VIS/Host.py"
}
}
}
release_info.compiler selects the C compiler Nuitka hands its generated code to
— msvc or clang on Windows, gcc or clang on Linux. It is optional;
omitting it uses the platform default. See Choosing the C compiler.
Per-screen fields:
Field |
Type |
Description |
|---|---|---|
|
string |
Python script filename in the project root |
|
bool |
Whether a standalone screen is included in a release (and gets a Start Menu shortcut); tabbed screens always ship |
|
string/null |
Icon name (without extension) from |
|
string/null |
Title-bar icon override; only honoured for standalone screens |
|
string |
Screen description |
|
string/null |
Documentation URL for this screen; |
|
bool |
|
|
bool |
Prevent duplicate tabs when |
|
string |
Screen-specific version ( |
|
string/null |
Free-form current-state string |
Window icons#
Windows keeps two icon slots per window: ICON_SMALL (title bar, alt-tab) and
ICON_BIG (taskbar). icon — per screen, or defaults.icon project-wide —
always drives the taskbar image. window_icon drives the title bar independently.
defaults.window_icon applies to every window in the project. A per-screen
window_icon overrides it, but only when that screen owns a standalone (chromeless)
window: a tabbed screen shares a chromed window that may host other screens, so its
window_icon is ignored by design.
Both fields are optional. Omitting them preserves the older behaviour where the title bar and the taskbar share one image. Off Windows the split is a no-op.
Application Settings#
Per-project application settings (0.6.0) live in .VIS/settings.json and are
reached through project.Settings, a
ProjectSettings:
from VIStk.Structures._Project import Project
settings = Project().Settings
settings.get("notifications.duration_ms") # 5000 (framework default)
settings.get("my.custom.key", "fallback") # explicit per-call default
settings.set("appearance.font_family", "Consolas")
settings.save() # persist to .VIS/settings.json
Lookup order for get is: stored override → explicit default argument → the
framework DEFAULTS table → None. The built-in keys cover window size, alignment
and minimums, geometry and open-tab restore, launch-time font, and notification defaults.
A complete settings.json (every key at its default) is written at VIS new and on
first Host launch, so the available options are discoverable and hand-editable. A missing
or corrupt file falls back to defaults rather than crashing. The Host saves settings
automatically on shutdown, and only after every window has closed without veto — a
cancelled quit leaves the file untouched.
Settings window#
Set host.settings_menu to true in project.json to add a framework-provided
Settings entry to every window’s HostMenu. It opens a tabbed settings surface —
as a single-instance tab where there is a chromed window to host one, otherwise as a modal
window — whose General tab edits the built-in keys, with Save, Cancel and Restore
Defaults.
Apps extend it with their own tabs:
def my_panel(parent): # parent is the tab's ttk.Frame
ttk.Checkbutton(parent, text="Enable widget X").pack(anchor="w")
host.register_settings_panel("My Plugin", my_panel)
Register panels in .VIS/Host.py before entering the update loop. Appearance changes
apply on the next launch; live re-styling of open windows is planned.
See Structures for the full ProjectSettings API, Widgets for
SettingsWindow, and Objects for register_settings_panel.