# horseman: the Headless Horseman

Run programs and windows **headless**, and drive them from code. Everything happens on a private hidden desktop
that your program creates: the windows there are real (they lay out, run their dialogs, answer messages), they just
never show on the screen, and programs started there get no console window either. Made for testing GUI programs
and for automating ones that insist on asking questions. Windows only.

```
basher install horseman
```

```python
import horseman

p = horseman.start("setup.exe --quiet")      # runs on the hidden desktop
w = horseman.find("Setup")                   # waits up to 15 s for a window with that title
horseman.click(horseman.child(w, "Button", "&Next >"))
print(horseman.wait(p, 60000))               # exit code (-1 if still running after 60 s)
```

## Functions

| function | what it does |
|---|---|
| `headless()` | from now on, this program's OWN windows (dialogs, gui) open on the hidden desktop. Call it before opening any window |
| `start(cmd, dir="") -> int` | start a program on the hidden desktop; returns its handle. A program named without a folder is looked for in `dir` (or the current folder) first |
| `wait(program, timeout_ms=-1) -> int` | its exit code, or -1 if it is still running after `timeout_ms` (-1 = wait for ever) |
| `running(program) -> bool` | still running? |
| `kill(program)` | end it |
| `find(title, timeout_ms=15000) -> int` | the window with exactly that title on the hidden desktop, waiting for it to appear; 0 if it never did. Waits half a second more after finding it so the window has finished filling itself in |
| `gone(window, timeout_ms=5000) -> bool` | wait until that window has closed (false if it's still there). Use it between two boxes with the same title, or `find()` can catch the old one on its way out |
| `child(window, cls="", text="", n=0, timeout_ms=2000) -> int` | a control inside `window` (any depth) by class (`"Button"`, `"Edit"`, `"Static"`) and/or exact text; `n` picks the n-th match. Waits for it (a window can show before all its controls exist); 0 if it never came |
| `click(control)` | click a button, the way a mouse click would |
| `press(window, button)` | send a dialog a button id: `horseman.OK`, `CANCEL`, `YES`, `NO`. A message box with only an OK button wants `CANCEL` (Esc) |
| `type(control, text)` | set a text box's text |
| `text(window) -> str` | a window's or control's text |
| `close(window)` | ask a window to close (like its X button) |
| `windows() -> list[str]` | titles of the visible windows on the hidden desktop: what is it showing right now? |
| `sleep(ms)` | wait |

Windows and programs are plain ints. Button texts include the `&` of their underlined letter (`"&Yes"`, `"&No"`).

## Good to know

- `find()` only sees the hidden desktop. Your program moves its own main thread there the first time it calls
  `find()` or `headless()`, which Windows only allows while the program has no window of its own on the real screen.
- Text goes into other programs only by `type()` (WM_SETTEXT), which Windows copies safely across programs. Most
  other window messages would hand the other program a pointer into YOUR memory and crash it, so there's no raw
  "send message" function.
- File dialogs keep filling themselves in for a moment after they appear: type the file name into
  `horseman.child(w, "Edit")` after `find()` (which already waits), then `press(w, horseman.OK)`.
- The hidden desktop goes away when your program ends; programs still running on it are not killed. `kill()` them.

## Example

`examples/drive_dialogs.bsh` starts the `dialogs` example and answers it.
