Metadata-Version: 2.4
Name: artemis-ui
Version: 0.2.1
Summary: A stupidly easy way to build Android and desktop apps in Python, on top of Flet.
License: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: flet>=0.86.0
Provides-Extra: icons
Requires-Dist: pillow; extra == "icons"
Provides-Extra: charts
Requires-Dist: flet-charts; extra == "charts"

# Artemis

A stupidly easy way to build Android and desktop apps in Python.

Artemis sits on top of [Flet](https://flet.dev) (which itself sits on
Flutter), and trims day-to-day app building down to something that reads
almost like pseudocode:

```python
import artemis as art

app = art.App("Hello", theme="ocean")

@app.page("/")
def home(page):
    return art.Column([
        art.Title("Hello, Artemis"),
        art.Button("Say hi", on_click=lambda e: print("hi!")),
    ], center=True)

app.run()
```

Run that, and you get a real, native-feeling app - not a wrapped webview -
with a Material 3 color scheme, correct fonts, and rounded modern-looking
controls, for free. Run the *exact same file* on Android with `flet build
apk` and it just works, because under the hood it's still Flet/Flutter the
whole way down.

Artemis isn't trying to replace Flet or compete with it - think of it as
the "batteries included" layer on top: opinionated defaults, a much
smaller vocabulary, and routing/state wired up so you rarely touch
`page.update()` yourself.

## Install

```bash
cd artemis          
pip install artemis-ui
```

Want the optional bits too (custom logo → `.ico` auto-conversion, and
charts)?

```bash
pip install -e ".[icons,charts]"
```

(Not published to PyPI yet - this is a first pass at the API. The
distribution name is `artemis-ui` to avoid clashing with the handful of
other "artemis" packages already on PyPI; you still `import artemis`.)

**Getting `ModuleNotFoundError: No module named 'artemis'`?** That means
Python can't see the package - almost always because you're running a
script from *inside* `examples/` while `pip install -e .` was never run,
or was run from the wrong folder. Two ways to fix it:

```bash
cd artemis
pip install artemis-ui
python examples/counter.py


cd artemis
python examples/counter.py
```

Either way, run the script from the `artemis/` project root (the one
containing both `artemis/` and `examples/`), not from inside `Examples/`
itself - a script can't `import` a package that's a sibling of its own
folder rather than its parent.

**Being upfront about a genuine Flet limitation:** while you're just
running `python examples/counter.py` (not a real build), the window icon
can only reliably be overridden on **Windows**, and only via an absolute
path to an actual `.ico` file - a relative path or a `.png` gets silently
ignored by Flet's pre-built desktop client (this is a known Flet quirk,
not something Artemis papers over - see `flet-dev/flet#3438`). Artemis
handles that correctly under the hood: absolute path, real `.ico`, done.
On **macOS**, the dock icon during dev preview is baked into Flet's own
pre-built client and literally can't be changed short of building your
own white-labeled Flet client - not an Artemis limitation, a Flet one.
Either way, `flet build`/`flet pack` (a real build) get the icon right
on every platform via `icon.png` above.

**Want your own logo?** Drop your own `logo.png` in `assets/` and it's
used instead automatically. If Pillow is installed, Artemis also
auto-converts it into a matching `logo.ico` for you so your own branding
shows up in the Windows dev-preview window icon too, not just ours. If
Pillow isn't installed, Artemis falls back to its own default `.ico` for
that one narrow case (`pip install pillow` if you want your PNG
converted automatically) - or just supply your own `logo.ico` directly
and Artemis will leave it alone.

If you want a different filename or a different assets folder entirely:

```python
app = art.App("My App", logo="brand.png")   
app.run(assets_dir="static")                
```

## The mental model

- **One `App`.** You make one, give it a title and a theme.
- **Pages are just functions.** Decorate a function with `@app.page("/route")`
  and return whatever control tree that screen should show.
- **State is a box, not magic.** `art.State(0)` gives you something you can
  mutate from inside a closure. Change `.value` and the screen updates -
  no signals, no dependency graph to reason about.
- **Buttons/switches/sliders auto-redraw.** After their `on_click` /
  `on_change` fires, Artemis quietly re-runs your page function and
  refreshes the screen. You just change state and move on.
- **Text inputs are the one exception.** Redrawing the whole page on every
  keystroke would drop your cursor position, so `Input()` takes a `bind=`
  State instead and updates it silently, no redraw, no dropped focus.

## What actually makes this different from plain Flet

Flet gives you the primitives; Artemis makes the everyday app-shell stuff
(the part every mobile app needs and every Flet tutorial re-explains from
scratch) a one-liner:

- **A real navigation stack, not a page swap.** `app.go("/details")` pushes
  a proper `flet.View` onto the stack with a back arrow that appears on
  its own. Android's hardware back button and the browser back button both
  pop it correctly - `app.back()` does the same thing in code. Plenty of
  "simple Flet wrapper" libraries just clear `page.controls`, which quietly
  breaks both of those.
- **`app.bottom_nav([...])`** gives you a persistent tab bar across your
  root screens - tap a tab, the root screen swaps; push into a detail view
  with `app.go()` and the tab bar correctly disappears until you go back,
  same as it would in a native app.
- **`art.Box(glass=True)`** - an instant frosted-glass panel (blur, faint
  border, translucent fill). In raw Flet that's `ft.Blur` + `ft.BoxShadow`
  + a manually opacity-adjusted `bgcolor`, three separate imports to get
  one visual effect; here it's a keyword argument.
- **`art.Box(gradient=["#6366F1", "#EC4899"])`** - builds the
  `ft.LinearGradient` for you instead of you constructing one by hand.
- **`app.toast("Saved!")`** - a one-line snackbar. No `page` argument to
  pass around, no remembering that `SnackBar` is technically a dialog-like
  control under the hood.

## Power features

The stuff above is the everyday layer. This is the stuff that makes
Artemis a framework rather than a widget-wrapper library.

### Route params

```python
@app.page("/user/:id")
def profile(page, params):
    return art.Text(f"User #{params['id']}")

app.go("/user/42") 
```

Your handler only receives `params` if it actually asks for a second
argument - plain `def home(page):` handlers keep working untouched.

### Responsive layouts (one codebase, phone and desktop)

```python
@app.page("/")
def home(page):
    return art.responsive(
        page,
        mobile=art.Column([...]),
        desktop=art.Row([...]),
        desktop_at=700,
    )
```

Artemis re-renders the current screen whenever the window resizes, so
this picks up live changes as you resize a desktop window - genuinely
one codebase behaving differently on a phone versus a desktop window,
which is the whole point of an "Android and desktop" library.

### Forms with real validation

```python
email = art.Field("", art.validators.required(), art.validators.email())
password = art.Field("", art.validators.required(), art.validators.min_length(6))
form = art.Form(email=email, password=password)

art.Input(label="Email", field=email)
art.Input(label="Password", field=password, password=True)
art.Button("Sign in", on_click=form.submit(lambda values: log_in(values["email"], values["password"])))
```

`form.submit(...)` only calls your function if every field passes;
otherwise each `Input` shows its own error message. Errors stay hidden
until a field's been touched (blurred, or a submit was attempted), so a
fresh form doesn't greet the user with a wall of red text. Built-in
validators: `required`, `email`, `min_length`, `max_length`, `matches`
(for "confirm password" fields), `number` - and they're just functions
that take a value and return an error string or `None`, so writing your
own is a five-line function.

### Persistent state (survives an app restart, no database needed)

```python
theme_pref = art.PersistentState("theme", default="indigo")
theme_pref.value = "forest"   # written to disk immediately
```

Same API as `State` - it's a drop-in swap - but backed by a small JSON
file in a `.artemis_data/` folder next to your script, so it's there the
next time your app starts. Good for remembering a theme choice, the last
tab someone was on, a small local list - not a replacement for a real
database once your data gets complicated.

### Charts

```python
art.LineChart([12, 19, 14, 24, 22, 30], labels=["Jan", "Feb", "Mar", "Apr", "May", "Jun"])
art.BarChart([12, 19, 8], labels=["Q1", "Q2", "Q3"])
art.PieChart({"Rent": 1200, "Food": 400, "Fun": 200})
```

Charts are an optional Flet add-on (`pip install flet-charts`) with a
fairly verbose raw API - manually built `DataPoint` objects, hand-wired
axis labels. These three functions take a plain list or dict and build
all of that for you. If `flet-charts` isn't installed, calling one of
these gives you a clear "pip install flet-charts" message instead of a
confusing import error.

### Live theme switching

```python
app.set_theme("forest")            
app.set_theme(dark_mode=True)     
```

Handy paired with `PersistentState` for a real "remember my theme"
settings screen - see `examples/dashboard.py`.

### The `artemis` CLI

```bash
artemis new "My Cool App"
cd "My Cool App"
pip install -e .
python main.py
```

Scaffolds a starter `main.py`, `pyproject.toml`, `assets/` folder, and
`.gitignore` - a real project layout instead of a blank file.

### A global error boundary

If a page function raises an exception - a bad route param, a typo in a
dict key, anything - Artemis shows a friendly "something went wrong"
screen with a "Go home" button instead of crashing the whole app. The
real traceback still prints to your console either way; this is just
what the *user* sees instead of a frozen or blank screen. This happens
automatically - there's nothing to configure.

### Async event handlers (file pickers, network calls, anything that waits)

Any `on_click`/`on_change` can be a plain function or an `async def` -
Flet natively awaits coroutine handlers, Artemis's auto-redraw wrapper
does too, so this just works:

```python
async def load_data(e):
    data = await fetch_something()
    results.value = data

art.Button("Load", on_click=load_data)
```

### Clipboard

```python
app.copy("https://example.com/shared")
art.Button("Paste", on_click=app.paste(lambda text: print("got:", text)))
```

### A side navigation drawer

```python
app.set_drawer([
    {"label": "Home", "icon": art.flet.Icons.HOME, "route": "/"},
    {"label": "Settings", "icon": art.flet.Icons.SETTINGS, "route": "/settings"},
])
```

Same idea as `bottom_nav`, but a side menu - better suited to a wide
desktop window than a bottom bar, which eats vertical space you don't
have to spare there. Shows a hamburger icon in the AppBar automatically
(standard Flutter behavior once a screen has a drawer attached, not
something Artemis wires up by hand).

### Page transitions

```python
app = art.App("My App", transitions="cupertino")  
```

Applies to every `app.go()` / `app.back()` push and pop, on every
platform, in one keyword.

### Date & time pickers

```python
art.Button("Pick a date", on_click=app.pick_date(lambda d: print(d)))
art.Button("Pick a time", on_click=app.pick_time(lambda t: print(t)))
```

### In-page tabs

```python
art.Tabs([
    ("Overview", art.Text("...")),
    ("Settings", art.Column([...])),
])
```

Not navigation between screens (that's `bottom_nav`/`set_drawer`) - just
switching which panel is visible on one screen. Raw Flet's `Tabs` needs a
separate `TabBar` + `TabBarView` kept in sync with a matching `length`
you update by hand; this is just a list of (label, content) pairs.

### Expandable sections

```python
art.Expandable("Shipping details", art.Text("Ships in 2-3 days."))
```

A collapsible section - FAQs, settings groups, "show more" details.

### Badges

```python
art.flet.Icon(art.flet.Icons.NOTIFICATIONS, badge=art.Badge("3", color="rose"))
```

Not a wrapper - a value you attach to another control's `badge`
property (most controls have one), the way Flet itself expects it.

### Keyboard shortcuts

```python
app.on_key("ctrl+s", lambda e: save())
app.on_key("escape", lambda e: app.back())
```

A global shortcut - handy for desktop apps. Modifiers are optional and
order doesn't matter (`"shift+ctrl+n"` and `"ctrl+shift+n"` both match).

### Grid layout

```python
art.Grid([art.Card(art.Text(p)) for p in products], columns=3)
art.Grid(tiles, min_item_width=160)  
```

### Network calls

```python
async def load(e):
    items.value = await art.fetch_json("https://api.example.com/items")

art.Button("Refresh", on_click=load)
```

`fetch_json`, `fetch_text`, and `post_json` - thin async wrappers around
`httpx` (already a Flet dependency, so this adds nothing new to
install), with a timeout and `raise_for_status` handled for you.

### Buttons with an automatic loading spinner

```python
saving = art.State(False)
art.Button("Save", on_click=save_handler, loading=saving)
```

Pass a `State` via `loading=` and the button shows a small spinner
instead of its label while an async `on_click` is running, and disables
itself so it can't be double-tapped. Artemis flips `saving.value` for
you before and after - read it elsewhere on the page too if you want a
loading indicator to show up in more than one place at once.

### Route guards

```python
app.page("/admin", guard=lambda: current_user.is_admin, redirect="/login")
```

`guard` is a zero-arg function checked right before that screen renders;
return `False` and Artemis shows `redirect` instead. Saves repeating an
auth check in every protected page function.

### Loading data without the boilerplate

Every screen that loads data ends up needing the same three things - a
loading flag, an error slot, a value slot - and since Artemis re-runs
your page function on every click *anywhere* in the app, you also have
to make sure you don't accidentally re-fetch on every single one of
those re-renders. `AsyncData` handles all of it:

```python
products = art.AsyncData(lambda: art.fetch_json(URL))

@app.page("/")
def home(page):
    products.render(page)  # fetches once, no-ops on every re-render after

    if products.loading:
        return art.Loader()
    if products.error:
        return art.Text(f"Couldn't load that: {products.error}")
    return art.Column([art.Text(p) for p in products.value])
```

Call `products.reset()` (e.g. from a "refresh" button) to force the next
`render()` to fetch again.

### Toasts with an action button

```python
app.toast("Task deleted", action="Undo", on_action=lambda e: restore(task))
```

### Testing your app without opening a window

```python
from artemis.testing import TestApp

def test_counter():
    t = TestApp(app).build()
    assert t.has_text("0")
    t.click(t.find_button("+"))
    assert t.has_text("1")
```

`TestApp` gives your `App` a fake page (views, dialogs, clipboard, all
of it) so your actual page functions and event handlers run for real,
no Flet client required - useful in CI, and just as useful while you're
building the thing so you're not re-clicking through the UI by hand
after every change. See `tests/test_examples.py` for a full pytest
suite against two of the example apps - run it with `pytest tests/`.

## What's in the box right now

`Text`, `Title`, `Button`, `Input`, `Switch`, `Checkbox`, `Slider`,
`Dropdown`, `Column`, `Row`, `Grid`, `Tabs`, `Expandable`, `Badge`, `Box`,
`Card`, `Spacer`, `Divider`, `Image`, `BottomNav`, `ListTile`, `Avatar`,
`Loader`, `ProgressBar`, `toast`, plus `App`, `State`, `PersistentState`,
`Field`, `Form`, `validators`, `responsive`, and
`LineChart`/`BarChart`/`PieChart`. `App` also gives you `app.alert(...)`,
`app.confirm(...)`, `app.set_theme(...)`, `app.set_drawer(...)`,
`app.copy(...)`/`app.paste(...)`, `app.pick_date(...)`/`app.pick_time(...)`,
and `app.on_key(...)`. Plus `art.fetch_json`/`fetch_text`/`post_json` for
network calls, `art.AsyncData` for the load/loading/error pattern,
`Button(loading=...)` for automatic spinners, `toast(action=...)`, route
guards, and `artemis.testing.TestApp` for testing without a window.
That's genuinely most of what a typical small-to-medium app needs.
Anything Artemis doesn't wrap yet, you can still reach for - `import
flet` (or `art.flet`) is right there, and every Artemis widget returns a
plain Flet control, so mixing the two is completely fine.

## Themes

**31 named palettes** ship out of the box, grouped roughly by mood:

- cool: `indigo`, `ocean`, `sky`, `cobalt`, `royal`, `teal`, `cyan`,
  `slate`, `steel`, `midnight`, `graphite`
- green: `forest`, `mint`, `emerald`, `lime`, `olive`
- warm: `sunset`, `rose`, `cherry`, `crimson`, `amber`, `gold`, `orange`,
  `coral`, `clay`, `sand`
- bold: `grape`, `violet`, `magenta`, `fuchsia`, `plum`

Or skip the list entirely and pass any hex string of your own, e.g.
`theme="#22D3EE"`. Each one is a Material 3 "seed color", so light mode,
dark mode, hover states, and text contrast are all derived automatically
- that's what makes a two-line Artemis app not look like default grey
Flet. `App(..., dark_mode=True/False)` forces a mode; leave it out and
Artemis follows the system setting.

### Full manual control - background, surface, text, and accent colors

For anyone who wants an exact look rather than a named vibe, four more
keywords give you direct control over the actual colors, not just the
seed:

```python
app = art.App(
    "My App",
    theme="indigo",          # still drives whatever you don't override below
    background="#0B1220",    # the color behind everything
    surface="#161F32",       # the color of cards/boxes on that background
    text="#E5E7EB",          # the default foreground/text color
    primary="#818CF8",       # the accent color for buttons, switches, etc.
)
```

Each one you set becomes a fixed, absolute color - in both light and
dark mode, since that's the point of overriding it - while anything you
leave as `None` still comes from the seed color, light/dark included.
This isn't an Artemis trick sitting on top of Flet; it's exactly how
Flet's own `ColorScheme` overriding works (`color_scheme_seed` fills in
the palette, individual `ColorScheme` fields you set take precedence) -
Artemis just gives you four plain keywords instead of making you build a
`ColorScheme` object by hand.

All of the above also works live, mid-app, via `app.set_theme(...)` -
same keywords, and anything you don't pass keeps its current value:

```python
app.set_theme("forest")                  # just change the palette
app.set_theme(primary="#EF4444")         # just nudge the accent color
app.set_theme(background=None, surface=None, text=None)  # clear overrides, back to pure seed
```

See `examples/theming.py` for all of this side by side.

## Examples

- `examples/counter.py` - the classic, shows State + auto-redraw
- `examples/todo.py` - a small task list, shows `Input(bind=...)`, dynamic
  lists of Cards, and Checkbox toggling
- `examples/theming.py` - named palettes vs full manual background /
  surface / text / primary control, switchable live
- `examples/showcase.py` - navigation (`app.go`/`app.back`), a bottom tab
  bar, glass + gradient panels, and a toast
- `examples/contacts.py` - `ListTile` + `Avatar` rows, `Loader` /
  `ProgressBar`, and `app.alert()` / `app.confirm()` dialogs
- `examples/login.py` - `Field` + `Form` + `validators` for real form
  validation
- `examples/dashboard.py` - route params, charts, a responsive layout,
  and a theme preference that survives an app restart
- `examples/drawer_and_clipboard.py` - a side navigation drawer, async
  event handlers, clipboard, a Grid layout, and a custom page transition
- `examples/tabs_and_shortcuts.py` - in-page Tabs, Expandable sections,
  a Badge, date/time pickers, and a global Ctrl+S keyboard shortcut
- `examples/network_and_guards.py` - a real `art.fetch_json` call, an
  automatic loading spinner, and a route guard/redirect
- `examples/async_data_demo.py` - `art.AsyncData` (load once, not on
  every re-render) and `toast()` with an Undo action button

```bash
python examples/counter.py
```

## Shipping to Android / desktop

This is the part Artemis doesn't try to reinvent - it's just Flet, so
Flet's own build tooling applies unchanged:

```bash
flet build apk     
flet build ipa       
flet build macos     
flet build windows      
flet build linux      
```

## Known rough edges (being upfront about it)

- Every button/switch click re-runs the current screen's function and
  redraws it from scratch. It's simple and fast enough for typical apps,
  but if you're rendering something genuinely huge (thousands of rows)
  you'll feel it. Scoped/partial updates are on the list for a v2.
- `bottom_nav()` assumes a single level of tabs; nesting a second tab bar
  inside a pushed screen isn't supported.
- `PersistentState` is plain JSON on disk next to your script - fine for
  small preferences and lists, not a real database once your data gets
  relational or large.
- Charts need the optional `flet-charts` package installed separately
  (`pip install flet-charts` or `pip install -e ".[charts]"`); Artemis
  doesn't force it on projects that don't need charts.
- No file/save/folder picker. There was one; it got pulled. Flet's
  underlying `FilePicker` control has a real, still-open registration
  bug (`Unknown control: FilePicker`, `flet-dev/flet#6040`/`#6251`/
  `#6422`) that's especially bad on Python 3.14, and no amount of
  retrying or delaying from the Python side reliably fixed it across
  every environment it was tested against. Rather than ship something
  that works on some machines and silently fails on others, Artemis
  doesn't wrap it right now. If you need file access, `art.flet`'s raw
  `FilePicker` is still there if you want to experiment with it yourself
  - just know you're on the same ground Artemis pulled back from.
- Widget coverage is intentionally focused on the common cases, not
  exhaustive. Anything Artemis doesn't wrap yet is one `import flet`
  away - every Artemis widget returns a plain Flet control.
