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.
@@ -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.
@@ -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.