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 the while loop and prevents the Host from driving screen loop() hooks.

  • Do not call root.destroy() to quit — call host.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 of Project.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.* and modules.* are always imported as full dotted paths (import Screens.MyScreen.f_header), never with from or as.

  • from imports inside functions are only used in the __main__ guard (from Screens.root import root, frame).

configure_menu Pattern#

Screens contribute menu items to the Host’s menu bar via the configure_menu hook. Items are added when the tab activates and automatically cleared when it deactivates.

def configure_menu(menubar):
    menubar.set_screen_items([
        {"label": "Export PDF", "command": export_pdf},
        {"label": "Print",      "command": print_fn},
    ], label="Work Orders")

    menubar.set_screen_items([
        {"label": "About", "command": show_about},
    ], label="Help")

Call set_screen_items multiple times to contribute multiple cascades.

Project-wide menus are configured in Host.py via default_menu_setup. This callback is called on every new DetachedWindow’s HostMenu, ensuring consistent menus across all windows:

host = Host()
host.default_menu_setup = lambda m: m.build_shared_menu({
    "File": [
        {"label": "New", "items": [...]},
        {"separator": True},
        {"label": "Exit", "command": host.quit_host},
    ],
    "Edit": [...],
    "View": [...],
    "Tools": [...],
})

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

script

string

Python script filename in the project root

release

bool

Whether a standalone screen is included in a release (and gets a Start Menu shortcut); tabbed screens always ship

icon

string/null

Icon name (without extension) from Icons/

window_icon

string/null

Title-bar icon override; only honoured for standalone screens

desc

string

Screen description

docs

string/null

Documentation URL for this screen; null falls back to defaults.docs

tabbed

bool

true = Host tab, false = its own chromeless window

single_instance

bool

Prevent duplicate tabs when true

version

string

Screen-specific version (major.minor.patch)

current

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.