folderui 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- folderui-0.2.0/PKG-INFO +181 -0
- folderui-0.2.0/README.md +162 -0
- folderui-0.2.0/examples/auction.py +385 -0
- folderui-0.2.0/examples/auction_bot.py +37 -0
- {folderui-0.1.0 → folderui-0.2.0}/examples/guess.py +14 -10
- {folderui-0.1.0 → folderui-0.2.0}/pyproject.toml +1 -1
- folderui-0.2.0/src/folderui/__init__.py +5 -0
- {folderui-0.1.0 → folderui-0.2.0}/src/folderui/fsutil.py +1 -11
- folderui-0.2.0/src/folderui/session.py +53 -0
- folderui-0.2.0/src/folderui/ui.py +252 -0
- folderui-0.2.0/tests/test_folderui.py +195 -0
- folderui-0.1.0/PKG-INFO +0 -140
- folderui-0.1.0/README.md +0 -121
- folderui-0.1.0/examples/auction.py +0 -368
- folderui-0.1.0/examples/auction_bot.py +0 -32
- folderui-0.1.0/src/folderui/__init__.py +0 -6
- folderui-0.1.0/src/folderui/server.py +0 -68
- folderui-0.1.0/src/folderui/session.py +0 -44
- folderui-0.1.0/src/folderui/ui.py +0 -261
- folderui-0.1.0/tests/test_folderui.py +0 -166
- {folderui-0.1.0 → folderui-0.2.0}/.gitignore +0 -0
- {folderui-0.1.0 → folderui-0.2.0}/LICENSE +0 -0
- {folderui-0.1.0 → folderui-0.2.0}/src/folderui/py.typed +0 -0
folderui-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: folderui
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Turn a folder into an immediate-mode GUI for humans and AI agents: *.md files are the view, *.sh files are buttons.
|
|
5
|
+
Project-URL: Homepage, https://github.com/xiao-e-yun/folderui
|
|
6
|
+
Project-URL: Repository, https://github.com/xiao-e-yun/folderui
|
|
7
|
+
Author: xiao-e-yun
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: agent,gui,immediate-mode,llm,multiplayer,turn-based
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: POSIX
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# folderui
|
|
21
|
+
|
|
22
|
+
Turn **a folder into an immediate-mode GUI** for humans and AI agents. The folder holds only two kinds of things:
|
|
23
|
+
|
|
24
|
+
- `*.md` and other files: the screen (`cat` them any time)
|
|
25
|
+
- `*.sh`: buttons (only the ones you can press right now exist)
|
|
26
|
+
|
|
27
|
+
Running a button **blocks until you have the next decision to make**, then prints whatever the app decides
|
|
28
|
+
(the result of your action, what happened meanwhile, what you can do now). The framework prints nothing of its own.
|
|
29
|
+
So an agent's loop is just "run a button → read the response → run the next button", with no extra waiting or polling.
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pip install folderui # or: uv add folderui. Standard library only; needs a POSIX shell.
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
```console
|
|
36
|
+
$ ./bid.sh 40 ← blocks while the other players are bidding
|
|
37
|
+
✅ You bid 40
|
|
38
|
+
📣 bob 45 (high bid: bob 45)
|
|
39
|
+
📣 carol drops out
|
|
40
|
+
👉 Your turn to bid: ./bid.sh <amount> (at least 50) or ./pass.sh
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Writing a UI
|
|
44
|
+
|
|
45
|
+
Subclasses implement `draw()` to declare the screen and `action()` to handle buttons; the optional `display()`
|
|
46
|
+
decides what the response says.
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from folderui import FolderUI
|
|
50
|
+
|
|
51
|
+
class Guess(FolderUI):
|
|
52
|
+
def draw(self): # re-declare the whole screen every frame; must not change state
|
|
53
|
+
self.file("rules.md", "Guess 1-100 with ./guess.sh <number>")
|
|
54
|
+
self.input("guess")
|
|
55
|
+
|
|
56
|
+
def action(self, name, args): # a button was pressed: return text (a rejection is just text, too)
|
|
57
|
+
...
|
|
58
|
+
return "too high"
|
|
59
|
+
|
|
60
|
+
def display(self, result): # optional: called once per response; result is the action's text
|
|
61
|
+
return f"{result}\n\n{len(self.tries)} guesses so far"
|
|
62
|
+
|
|
63
|
+
Guess("/tmp/my-ui").run()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
| Method | Called from | Purpose |
|
|
67
|
+
|---|---|---|
|
|
68
|
+
| `file(path, content)` | `draw()` | Declare a file; undeclared files are deleted |
|
|
69
|
+
| `input(path)` | `draw()` | Declare a button → `<path>.sh`; paths may be nested (`players/2/kick`). Document usage in your `.md` files |
|
|
70
|
+
| `close()` | `draw()` or anywhere | Finish: remove the buttons, answer the waiting script, return from `run()` |
|
|
71
|
+
| `refresh()` | any thread | The state was changed elsewhere: redraw this UI (does not block) |
|
|
72
|
+
| `run(background=False)` | | Run the main loop; with `background=True`, run it in a thread and return the thread |
|
|
73
|
+
| `inputs` | `display()` | Read-only list of the buttons available right now |
|
|
74
|
+
|
|
75
|
+
The response is `display(result)`; without a `display()` it is just the action's return value. Like HTTP:
|
|
76
|
+
|
|
77
|
+
- **exit 0** (200): the app answered. What the text says, including whether the action was accepted, is up to the app.
|
|
78
|
+
If `action()` or `display()` raises, that is the app's problem: the error is printed as the response (the full
|
|
79
|
+
traceback goes to the host's stderr).
|
|
80
|
+
- **exit 1** (4xx): `Operation Unavailable` plus the reason: another action is in flight, or the button is no longer on screen.
|
|
81
|
+
|
|
82
|
+
The framework never appends a button list or a summary of screen changes: with a large screen (a Blender bridge,
|
|
83
|
+
a map game) that is only noise, so `display()` decides what to tell the agent, e.g. "your turn, use ./bid.sh <amount>"
|
|
84
|
+
or "the screen changed, read shop.md".
|
|
85
|
+
|
|
86
|
+
`display()` runs exactly once per response, so it can remember what this participant has already seen and include
|
|
87
|
+
only new messages:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
def display(self, result):
|
|
91
|
+
with self.game.lock:
|
|
92
|
+
new, self.seen = self.game.events[self.seen:], len(self.game.events)
|
|
93
|
+
return "\n".join([result, *new, "Your turn"])
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**There is only one response rule**: after the action is handled, respond at the first frame that has a button
|
|
97
|
+
available (or after `close()`).
|
|
98
|
+
|
|
99
|
+
- Invalid input → state unchanged, buttons still there → respond immediately (the app's error text)
|
|
100
|
+
- Queries → buttons still there → respond immediately
|
|
101
|
+
- Waiting for others after acting → no buttons → wait until it's your turn
|
|
102
|
+
|
|
103
|
+
## Multiplayer: shared state + a lock + refresh()
|
|
104
|
+
|
|
105
|
+
There is no server. Each participant gets a FolderUI, and they all share one game object:
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
class Game:
|
|
109
|
+
def __init__(self):
|
|
110
|
+
self.lock = threading.Lock()
|
|
111
|
+
self.uis = []
|
|
112
|
+
...
|
|
113
|
+
|
|
114
|
+
def bid(self, who, amount):
|
|
115
|
+
with self.lock:
|
|
116
|
+
... # pure logic; return an error text if the rules forbid it
|
|
117
|
+
for ui in self.uis:
|
|
118
|
+
ui.refresh() # only marks "needs redraw"; never blocks
|
|
119
|
+
return "✅ You bid"
|
|
120
|
+
|
|
121
|
+
class PlayerUI(FolderUI):
|
|
122
|
+
def draw(self):
|
|
123
|
+
with self.game.lock: # read the shared state directly
|
|
124
|
+
v = self.game.view(self.me)
|
|
125
|
+
...
|
|
126
|
+
|
|
127
|
+
def action(self, name, args):
|
|
128
|
+
return self.game.bid(self.me, int(args[0]))
|
|
129
|
+
|
|
130
|
+
game = Game()
|
|
131
|
+
game.uis = [PlayerUI(f"{root}/player_{i}", game, i) for i in range(n)]
|
|
132
|
+
for t in [ui.run(background=True) for ui in game.uis]:
|
|
133
|
+
t.join()
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Each UI draws its screen and handles its buttons on its own thread; `refresh()` is the only method meant to be
|
|
137
|
+
called across threads.
|
|
138
|
+
|
|
139
|
+
## Driving a UI from code
|
|
140
|
+
|
|
141
|
+
`Session` speaks the same file protocol as the button scripts, for bots, tests, or your own tool loop:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from folderui import Session
|
|
145
|
+
s = Session("/tmp/folderui-auction-xxxx/player_0")
|
|
146
|
+
s.ready()
|
|
147
|
+
print(s.read("rules.md"))
|
|
148
|
+
while inputs := s.inputs(): # buttons available right now; empty means closed
|
|
149
|
+
ok, text = s.input("bid", "40")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Details
|
|
153
|
+
|
|
154
|
+
- Scripts wait indefinitely and never time out on their own. If the agent's command is killed by a timeout, the
|
|
155
|
+
request has already been sent: **run the same command again to keep waiting or get the response** (it is not
|
|
156
|
+
sent twice); running a different button is rejected (exit code 1). The button's script is kept until its
|
|
157
|
+
response has been read.
|
|
158
|
+
- Pressing a button that is no longer on screen (the screen was redrawn just as the script ran) gets an immediate
|
|
159
|
+
"Operation Unavailable" response from the framework (exit code 1), without calling `action()` or `display()`.
|
|
160
|
+
- Concurrent presses in the same folder are serialized by a `.lock` directory; a press that finds it held is
|
|
161
|
+
rejected (exit code 1).
|
|
162
|
+
- Protocol files are hidden: `.request`, `.response`, `.done`, `.lock`, `.state.json`.
|
|
163
|
+
An agent that cannot run scripts can write `.request` directly (button path and arguments, NUL-separated).
|
|
164
|
+
|
|
165
|
+
## Examples
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
uv run python examples/guess.py # single player: guess the number
|
|
169
|
+
uv run python examples/auction.py --players 4 --rounds 3 # multiplayer: English auction (players + spectator)
|
|
170
|
+
uv run python examples/auction_bot.py <folder> bot0 # a random bot for the auction
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Development
|
|
174
|
+
|
|
175
|
+
```sh
|
|
176
|
+
uv sync
|
|
177
|
+
uv run pytest
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Releasing: configure a trusted publisher on PyPI (repo `xiao-e-yun/folderui`, workflow `publish.yml`,
|
|
181
|
+
environment `pypi`), then bump the version in `pyproject.toml` and push a `v*` tag to publish automatically.
|
folderui-0.2.0/README.md
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# folderui
|
|
2
|
+
|
|
3
|
+
Turn **a folder into an immediate-mode GUI** for humans and AI agents. The folder holds only two kinds of things:
|
|
4
|
+
|
|
5
|
+
- `*.md` and other files: the screen (`cat` them any time)
|
|
6
|
+
- `*.sh`: buttons (only the ones you can press right now exist)
|
|
7
|
+
|
|
8
|
+
Running a button **blocks until you have the next decision to make**, then prints whatever the app decides
|
|
9
|
+
(the result of your action, what happened meanwhile, what you can do now). The framework prints nothing of its own.
|
|
10
|
+
So an agent's loop is just "run a button → read the response → run the next button", with no extra waiting or polling.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
pip install folderui # or: uv add folderui. Standard library only; needs a POSIX shell.
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```console
|
|
17
|
+
$ ./bid.sh 40 ← blocks while the other players are bidding
|
|
18
|
+
✅ You bid 40
|
|
19
|
+
📣 bob 45 (high bid: bob 45)
|
|
20
|
+
📣 carol drops out
|
|
21
|
+
👉 Your turn to bid: ./bid.sh <amount> (at least 50) or ./pass.sh
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Writing a UI
|
|
25
|
+
|
|
26
|
+
Subclasses implement `draw()` to declare the screen and `action()` to handle buttons; the optional `display()`
|
|
27
|
+
decides what the response says.
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from folderui import FolderUI
|
|
31
|
+
|
|
32
|
+
class Guess(FolderUI):
|
|
33
|
+
def draw(self): # re-declare the whole screen every frame; must not change state
|
|
34
|
+
self.file("rules.md", "Guess 1-100 with ./guess.sh <number>")
|
|
35
|
+
self.input("guess")
|
|
36
|
+
|
|
37
|
+
def action(self, name, args): # a button was pressed: return text (a rejection is just text, too)
|
|
38
|
+
...
|
|
39
|
+
return "too high"
|
|
40
|
+
|
|
41
|
+
def display(self, result): # optional: called once per response; result is the action's text
|
|
42
|
+
return f"{result}\n\n{len(self.tries)} guesses so far"
|
|
43
|
+
|
|
44
|
+
Guess("/tmp/my-ui").run()
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Method | Called from | Purpose |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `file(path, content)` | `draw()` | Declare a file; undeclared files are deleted |
|
|
50
|
+
| `input(path)` | `draw()` | Declare a button → `<path>.sh`; paths may be nested (`players/2/kick`). Document usage in your `.md` files |
|
|
51
|
+
| `close()` | `draw()` or anywhere | Finish: remove the buttons, answer the waiting script, return from `run()` |
|
|
52
|
+
| `refresh()` | any thread | The state was changed elsewhere: redraw this UI (does not block) |
|
|
53
|
+
| `run(background=False)` | | Run the main loop; with `background=True`, run it in a thread and return the thread |
|
|
54
|
+
| `inputs` | `display()` | Read-only list of the buttons available right now |
|
|
55
|
+
|
|
56
|
+
The response is `display(result)`; without a `display()` it is just the action's return value. Like HTTP:
|
|
57
|
+
|
|
58
|
+
- **exit 0** (200): the app answered. What the text says, including whether the action was accepted, is up to the app.
|
|
59
|
+
If `action()` or `display()` raises, that is the app's problem: the error is printed as the response (the full
|
|
60
|
+
traceback goes to the host's stderr).
|
|
61
|
+
- **exit 1** (4xx): `Operation Unavailable` plus the reason: another action is in flight, or the button is no longer on screen.
|
|
62
|
+
|
|
63
|
+
The framework never appends a button list or a summary of screen changes: with a large screen (a Blender bridge,
|
|
64
|
+
a map game) that is only noise, so `display()` decides what to tell the agent, e.g. "your turn, use ./bid.sh <amount>"
|
|
65
|
+
or "the screen changed, read shop.md".
|
|
66
|
+
|
|
67
|
+
`display()` runs exactly once per response, so it can remember what this participant has already seen and include
|
|
68
|
+
only new messages:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
def display(self, result):
|
|
72
|
+
with self.game.lock:
|
|
73
|
+
new, self.seen = self.game.events[self.seen:], len(self.game.events)
|
|
74
|
+
return "\n".join([result, *new, "Your turn"])
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**There is only one response rule**: after the action is handled, respond at the first frame that has a button
|
|
78
|
+
available (or after `close()`).
|
|
79
|
+
|
|
80
|
+
- Invalid input → state unchanged, buttons still there → respond immediately (the app's error text)
|
|
81
|
+
- Queries → buttons still there → respond immediately
|
|
82
|
+
- Waiting for others after acting → no buttons → wait until it's your turn
|
|
83
|
+
|
|
84
|
+
## Multiplayer: shared state + a lock + refresh()
|
|
85
|
+
|
|
86
|
+
There is no server. Each participant gets a FolderUI, and they all share one game object:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
class Game:
|
|
90
|
+
def __init__(self):
|
|
91
|
+
self.lock = threading.Lock()
|
|
92
|
+
self.uis = []
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
def bid(self, who, amount):
|
|
96
|
+
with self.lock:
|
|
97
|
+
... # pure logic; return an error text if the rules forbid it
|
|
98
|
+
for ui in self.uis:
|
|
99
|
+
ui.refresh() # only marks "needs redraw"; never blocks
|
|
100
|
+
return "✅ You bid"
|
|
101
|
+
|
|
102
|
+
class PlayerUI(FolderUI):
|
|
103
|
+
def draw(self):
|
|
104
|
+
with self.game.lock: # read the shared state directly
|
|
105
|
+
v = self.game.view(self.me)
|
|
106
|
+
...
|
|
107
|
+
|
|
108
|
+
def action(self, name, args):
|
|
109
|
+
return self.game.bid(self.me, int(args[0]))
|
|
110
|
+
|
|
111
|
+
game = Game()
|
|
112
|
+
game.uis = [PlayerUI(f"{root}/player_{i}", game, i) for i in range(n)]
|
|
113
|
+
for t in [ui.run(background=True) for ui in game.uis]:
|
|
114
|
+
t.join()
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Each UI draws its screen and handles its buttons on its own thread; `refresh()` is the only method meant to be
|
|
118
|
+
called across threads.
|
|
119
|
+
|
|
120
|
+
## Driving a UI from code
|
|
121
|
+
|
|
122
|
+
`Session` speaks the same file protocol as the button scripts, for bots, tests, or your own tool loop:
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from folderui import Session
|
|
126
|
+
s = Session("/tmp/folderui-auction-xxxx/player_0")
|
|
127
|
+
s.ready()
|
|
128
|
+
print(s.read("rules.md"))
|
|
129
|
+
while inputs := s.inputs(): # buttons available right now; empty means closed
|
|
130
|
+
ok, text = s.input("bid", "40")
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Details
|
|
134
|
+
|
|
135
|
+
- Scripts wait indefinitely and never time out on their own. If the agent's command is killed by a timeout, the
|
|
136
|
+
request has already been sent: **run the same command again to keep waiting or get the response** (it is not
|
|
137
|
+
sent twice); running a different button is rejected (exit code 1). The button's script is kept until its
|
|
138
|
+
response has been read.
|
|
139
|
+
- Pressing a button that is no longer on screen (the screen was redrawn just as the script ran) gets an immediate
|
|
140
|
+
"Operation Unavailable" response from the framework (exit code 1), without calling `action()` or `display()`.
|
|
141
|
+
- Concurrent presses in the same folder are serialized by a `.lock` directory; a press that finds it held is
|
|
142
|
+
rejected (exit code 1).
|
|
143
|
+
- Protocol files are hidden: `.request`, `.response`, `.done`, `.lock`, `.state.json`.
|
|
144
|
+
An agent that cannot run scripts can write `.request` directly (button path and arguments, NUL-separated).
|
|
145
|
+
|
|
146
|
+
## Examples
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
uv run python examples/guess.py # single player: guess the number
|
|
150
|
+
uv run python examples/auction.py --players 4 --rounds 3 # multiplayer: English auction (players + spectator)
|
|
151
|
+
uv run python examples/auction_bot.py <folder> bot0 # a random bot for the auction
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```sh
|
|
157
|
+
uv sync
|
|
158
|
+
uv run pytest
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Releasing: configure a trusted publisher on PyPI (repo `xiao-e-yun/folderui`, workflow `publish.yml`,
|
|
162
|
+
environment `pypi`), then bump the version in `pyproject.toml` and push a `v*` tag to publish automatically.
|