folderui 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ """folderui:把資料夾變成給人類與 AI agent 用的立即式 GUI。"""
2
+ from .server import Server
3
+ from .session import Session
4
+ from .ui import FolderUI
5
+
6
+ __all__ = ["FolderUI", "Server", "Session"]
folderui/fsutil.py ADDED
@@ -0,0 +1,28 @@
1
+ """檔案工具:所有寫入都是原子的(先寫暫存檔再 rename),避免讀到半個檔案。"""
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def write_text(path: Path, text: str, mode: int | None = None) -> None:
8
+ tmp = path.with_name(f".{path.name}.tmp")
9
+ tmp.write_text(text, encoding="utf-8")
10
+ if mode is not None:
11
+ os.chmod(tmp, mode)
12
+ os.replace(tmp, path)
13
+
14
+
15
+ def write_json(path: Path, data) -> None:
16
+ write_text(path, json.dumps(data, ensure_ascii=False, indent=2) + "\n")
17
+
18
+
19
+ def read_text(path: Path, default=None):
20
+ try:
21
+ return path.read_text(encoding="utf-8")
22
+ except FileNotFoundError:
23
+ return default
24
+
25
+
26
+ def read_json(path: Path, default=None):
27
+ text = read_text(path)
28
+ return default if text is None else json.loads(text)
folderui/py.typed ADDED
File without changes
folderui/server.py ADDED
@@ -0,0 +1,68 @@
1
+ """多人用的伺服器:一個執行緒依序處理所有客戶端的請求,並推播給客戶端。
2
+
3
+ 客戶端 ──call(who, cmd)──▶ Server.handle() 請求/回應(同步)
4
+ 客戶端 ◀──push(who, msg)── Server 推播(非同步)
5
+
6
+ handle() raise 的例外會傳回呼叫端(ValueError 代表規則不允許)。
7
+ 推播走客戶端的無上限佇列,伺服器永遠不會等待客戶端,所以不會死鎖。
8
+ """
9
+ import queue
10
+ import threading
11
+ import traceback
12
+
13
+
14
+ class Server:
15
+ def __init__(self):
16
+ self.clients: dict = {}
17
+ self._requests: queue.Queue = queue.Queue()
18
+ self._stopped = False
19
+
20
+ # ---- 子類別實作 ----
21
+
22
+ def handle(self, who, cmd):
23
+ raise NotImplementedError
24
+
25
+ def on_start(self) -> None:
26
+ pass
27
+
28
+ # ---- API ----
29
+
30
+ def connect(self, who, client) -> None:
31
+ """註冊一個可以接收推播的客戶端(任何有 push(msg) 的物件)。"""
32
+ self.clients[who] = client
33
+
34
+ def call(self, who, cmd, timeout: float = 30):
35
+ box: queue.Queue = queue.Queue(maxsize=1)
36
+ self._requests.put((who, cmd, box))
37
+ try:
38
+ ok, value = box.get(timeout=timeout)
39
+ except queue.Empty:
40
+ raise TimeoutError("伺服器沒有回應") from None
41
+ if ok:
42
+ return value
43
+ raise value
44
+
45
+ def push(self, who, msg) -> None:
46
+ self.clients[who].push(msg)
47
+
48
+ def start(self) -> threading.Thread:
49
+ t = threading.Thread(target=self.run, name="folderui:server", daemon=True)
50
+ t.start()
51
+ return t
52
+
53
+ def stop(self) -> None:
54
+ self._stopped = True
55
+
56
+ def run(self) -> None:
57
+ self.on_start()
58
+ while not self._stopped:
59
+ try:
60
+ who, cmd, box = self._requests.get(timeout=0.2)
61
+ except queue.Empty:
62
+ continue
63
+ try:
64
+ box.put((True, self.handle(who, cmd)))
65
+ except Exception as e:
66
+ if not isinstance(e, ValueError):
67
+ traceback.print_exc()
68
+ box.put((False, e))
folderui/session.py ADDED
@@ -0,0 +1,44 @@
1
+ """程式型 agent 用的客戶端:和按鈕腳本走同一個檔案協定。"""
2
+ import json
3
+ import time
4
+ from pathlib import Path
5
+
6
+ from .fsutil import read_text, write_text
7
+
8
+
9
+ class Session:
10
+ def __init__(self, path: str | Path, poll: float = 0.1):
11
+ self.root = Path(path)
12
+ self.poll = poll
13
+
14
+ def ready(self, timeout: float | None = None) -> bool:
15
+ """等到資料夾畫好(第一次可以讀到按鈕列表)。"""
16
+ deadline = None if timeout is None else time.monotonic() + timeout
17
+ while not (self.root / ".buttons.json").exists():
18
+ if deadline is not None and time.monotonic() > deadline:
19
+ return False
20
+ time.sleep(self.poll)
21
+ return True
22
+
23
+ def buttons(self) -> dict[str, dict]:
24
+ """目前可按的按鈕 {路徑: {hint, options}};空的代表已結束(或還輪不到你)。"""
25
+ return json.loads(read_text(self.root / ".buttons.json", "{}"))
26
+
27
+ def read(self, path: str) -> str:
28
+ return read_text(self.root / path, "")
29
+
30
+ def press(self, path: str, *args: str, timeout: float | None = None) -> tuple[bool, str]:
31
+ """按下按鈕並等待回應,回傳 (是否成功, 回應內容)。"""
32
+ request = self.root / ".request"
33
+ if request.exists():
34
+ raise RuntimeError("上一個操作還在等待回應")
35
+ (self.root / ".done").unlink(missing_ok=True)
36
+ write_text(request, "".join(f"{x}\0" for x in (path, *args)))
37
+ deadline = None if timeout is None else time.monotonic() + timeout
38
+ while request.exists():
39
+ if deadline is not None and time.monotonic() > deadline:
40
+ raise TimeoutError("等待回應逾時")
41
+ time.sleep(self.poll)
42
+ code, _, text = read_text(self.root / ".response", "1\n").partition("\n")
43
+ (self.root / ".done").unlink(missing_ok=True)
44
+ return code.strip() == "0", text.rstrip("\n")
folderui/ui.py ADDED
@@ -0,0 +1,261 @@
1
+ """FolderUI:把一個資料夾變成給人類或 AI agent 用的立即式 GUI。
2
+
3
+ 子類別實作三個方法:
4
+ draw() 每一幀重新宣告畫面:file()、button()、reply()。不可修改狀態。
5
+ action(name, args) 按鈕被按下時呼叫:回傳文字,或 raise 代表失敗。只有這裡修改狀態。
6
+ receive(msg) 收到 push() 的訊息(例如伺服器推播)。
7
+
8
+ 資料夾裡只有 draw() 宣告的檔案和按鈕腳本(<path>.sh)。執行按鈕腳本後,腳本會一直等待,
9
+ 直到「操作處理完之後,第一個有按鈕可按的畫面」出現(或 close())才回應:
10
+ action 的回傳值 + 這段期間的 notify() + reply() + 目前可按的按鈕
11
+
12
+ 腳本與主機之間的協定(隱藏檔):
13
+ .request 腳本寫入:按鈕路徑與參數,以 NUL 分隔
14
+ .response 主機寫入:第一行為 exit code,之後為回應內容
15
+ .done 主機回應後把 .request 改名為 .done;腳本讀完回應後刪除它
16
+ .buttons.json 目前的按鈕列表(給程式型 agent 用)
17
+ """
18
+ import json
19
+ import os
20
+ import queue
21
+ import shlex
22
+ import sys
23
+ import threading
24
+ import time
25
+ import traceback
26
+ from pathlib import Path
27
+
28
+ from .fsutil import write_text
29
+
30
+ PROTOCOL_FILES = (".request", ".request.new", ".response", ".done")
31
+
32
+ SCRIPT = """\
33
+ #!/bin/sh
34
+ # folderui 按鈕:{name}{hint}
35
+ cd {root} || exit 1
36
+ printf '%s\\0' {qname} "$@" > .request.new
37
+ if [ -e .request ]; then
38
+ if cmp -s .request .request.new; then
39
+ rm -f .request.new
40
+ echo "⏳ 繼續等待…" >&2
41
+ else
42
+ rm -f .request.new
43
+ echo "❌ 上一個操作還在等待回應;再執行一次同樣的指令可以繼續等待"
44
+ exit 1
45
+ fi
46
+ elif [ -e .done ] && cmp -s .done .request.new; then
47
+ rm -f .request.new
48
+ else
49
+ rm -f .done .response
50
+ mv .request.new .request
51
+ echo "⏳ 已送出,等待中…" >&2
52
+ fi
53
+ limit=$(( ${{FOLDERUI_TIMEOUT:-0}} * 5 )); i=0
54
+ while [ -e .request ]; do
55
+ sleep 0.2; i=$((i+1))
56
+ if [ "$limit" -gt 0 ] && [ "$i" -ge "$limit" ]; then
57
+ echo "⏳ 還在等待中,再執行一次同樣的指令即可繼續等待"
58
+ exit 2
59
+ fi
60
+ done
61
+ code=$(head -n 1 .response)
62
+ tail -n +2 .response
63
+ rm -f .done
64
+ exit "$code"
65
+ """
66
+
67
+
68
+ def _check_path(path: str) -> str:
69
+ parts = path.split("/")
70
+ if path.startswith("/") or any(not p or p.startswith(".") for p in parts):
71
+ raise ValueError(f"路徑不能是絕對路徑、空段落或隱藏檔:{path!r}")
72
+ return path
73
+
74
+
75
+ class FolderUI:
76
+ poll = 0.1
77
+ close_grace = 5.0
78
+
79
+ def __init__(self, path: str | Path):
80
+ self.root = Path(path).resolve()
81
+ self.root.mkdir(parents=True, exist_ok=True)
82
+ self.closed = False
83
+ self._inbox: queue.Queue = queue.Queue()
84
+ self._files: dict[str, str] = {}
85
+ self._buttons: dict[str, tuple[str, list[str] | None]] = {}
86
+ self._reply = ""
87
+ self._disk_files: dict[str, str] = {} # 磁碟上由本框架管理的檔案
88
+ self._disk_scripts: dict[str, str] = {}
89
+ self._notes: list[str] = []
90
+ self._pending: tuple[int, str] | None = None
91
+ self._pending_button: tuple[str, str] | None = None # (路徑, hint):回應被讀走前保留它的腳本
92
+
93
+ # ---- 子類別實作 ----
94
+
95
+ def draw(self) -> None:
96
+ pass
97
+
98
+ def action(self, name: str, args: list[str]) -> str | None:
99
+ raise ValueError(f"沒有處理按鈕 {name}")
100
+
101
+ def receive(self, msg) -> None:
102
+ pass
103
+
104
+ # ---- 在 draw() 中呼叫 ----
105
+
106
+ def file(self, path: str, content: str) -> None:
107
+ self._files[_check_path(path)] = content
108
+
109
+ def button(self, path: str, hint: str = "", options: list[str] | None = None) -> None:
110
+ self._buttons[_check_path(path)] = (hint, options)
111
+
112
+ def reply(self, text: str) -> None:
113
+ self._reply = text
114
+
115
+ # ---- 在 action() / receive() 中呼叫 ----
116
+
117
+ def notify(self, text: str) -> None:
118
+ """累積一則消息,在下一次回應時一起送出。"""
119
+ self._notes.append(text)
120
+
121
+ def close(self) -> None:
122
+ """結束:移除所有按鈕,回應正在等待的腳本,然後停止。"""
123
+ self.closed = True
124
+
125
+ # ---- 給其他執行緒 ----
126
+
127
+ def push(self, msg) -> None:
128
+ self._inbox.put(msg)
129
+
130
+ def start(self) -> threading.Thread:
131
+ t = threading.Thread(target=self.run, name=f"folderui:{self.root.name}", daemon=True)
132
+ t.start()
133
+ return t
134
+
135
+ # ---- 主迴圈 ----
136
+
137
+ def run(self) -> None:
138
+ for name in PROTOCOL_FILES:
139
+ (self.root / name).unlink(missing_ok=True)
140
+ self._redraw()
141
+ deadline = None
142
+ while not (self.closed and self._pending is None and self._pending_button is None):
143
+ if self.closed and self._pending is None: # 已結束:最後的回應最多等 close_grace 秒被讀走
144
+ deadline = deadline or time.monotonic() + self.close_grace
145
+ if time.monotonic() > deadline:
146
+ break
147
+ changed = False
148
+ try:
149
+ msg = self._inbox.get(timeout=self.poll)
150
+ self._call(self.receive, msg)
151
+ changed = True
152
+ except queue.Empty:
153
+ pass
154
+ changed |= self._take_request()
155
+ changed |= self._drain()
156
+ if self._pending is None and self._pending_button and not (self.root / ".done").exists():
157
+ self._pending_button = None # 回應已被讀走
158
+ changed = True
159
+ if changed:
160
+ self._redraw()
161
+ if self._pending is not None and (self._buttons or self.closed):
162
+ self._respond()
163
+
164
+ def _drain(self) -> bool:
165
+ changed = False
166
+ while True:
167
+ try:
168
+ msg = self._inbox.get_nowait()
169
+ except queue.Empty:
170
+ return changed
171
+ self._call(self.receive, msg)
172
+ changed = True
173
+
174
+ def _take_request(self) -> bool:
175
+ if self._pending is not None:
176
+ return False
177
+ try:
178
+ raw = (self.root / ".request").read_bytes()
179
+ except FileNotFoundError:
180
+ return False
181
+ name, *args = [x.decode("utf-8", "replace") for x in raw.split(b"\0")[:-1]] or [""]
182
+ if name not in self._buttons:
183
+ self._pending = (1, f"❌ 按鈕 {name} 已經不存在")
184
+ return True
185
+ self._pending_button = (name, self._buttons[name][0])
186
+ try:
187
+ result = self.action(name, args)
188
+ self._pending = (0, result or "")
189
+ except Exception as e:
190
+ if not isinstance(e, ValueError): # ValueError 視為使用者錯誤,其餘是程式 bug
191
+ traceback.print_exc()
192
+ self._pending = (1, f"❌ {e}")
193
+ return True
194
+
195
+ def _respond(self) -> None:
196
+ code, text = self._pending
197
+ parts = [text] if text else []
198
+ parts += self._notes
199
+ if self._reply:
200
+ parts.append(self._reply)
201
+ if self.closed:
202
+ parts.append("── 已結束 ──")
203
+ else:
204
+ lines = ["── 可以做的事 ──"]
205
+ for path, (hint, options) in self._buttons.items():
206
+ lines.append(f"./{path}.sh" + (f" {hint}" if hint else ""))
207
+ if options:
208
+ lines.append(" 選項:" + " / ".join(options))
209
+ parts.append("\n".join(lines))
210
+ write_text(self.root / ".response", f"{code}\n" + "\n\n".join(parts) + "\n")
211
+ os.replace(self.root / ".request", self.root / ".done")
212
+ self._notes = []
213
+ self._pending = None
214
+
215
+ def _redraw(self) -> None:
216
+ self._files, self._buttons, self._reply = {}, {}, ""
217
+ self._call(self.draw)
218
+ if self.closed:
219
+ self._buttons = {}
220
+ scripts = {f"{p}.sh": self._script(p, hint) for p, (hint, _) in self._buttons.items()}
221
+ if self._pending_button: # 逾時後重新執行同一個指令=繼續等待或拿回應,所以腳本要留到回應被讀走
222
+ name, hint = self._pending_button
223
+ scripts.setdefault(f"{name}.sh", self._script(name, hint))
224
+ self._sync(self._disk_files, self._files, None)
225
+ self._sync(self._disk_scripts, scripts, 0o755)
226
+ write_text(self.root / ".buttons.json", json.dumps(
227
+ {p: {"hint": h, "options": o} for p, (h, o) in self._buttons.items()},
228
+ ensure_ascii=False, indent=2) + "\n")
229
+
230
+ def _sync(self, disk: dict[str, str], want: dict[str, str], mode: int | None) -> None:
231
+ for path in set(disk) - set(want):
232
+ (self.root / path).unlink(missing_ok=True)
233
+ self._prune(self.root / path)
234
+ del disk[path]
235
+ for path, content in want.items():
236
+ if disk.get(path) != content:
237
+ target = self.root / path
238
+ target.parent.mkdir(parents=True, exist_ok=True)
239
+ write_text(target, content, mode)
240
+ disk[path] = content
241
+
242
+ def _prune(self, path: Path) -> None:
243
+ for d in path.parents:
244
+ if d == self.root:
245
+ return
246
+ try:
247
+ d.rmdir()
248
+ except OSError:
249
+ return
250
+
251
+ def _script(self, name: str, hint: str) -> str:
252
+ hint = " ".join(hint.split())
253
+ return SCRIPT.format(name=name, hint=f" — {hint}" if hint else "",
254
+ root=shlex.quote(str(self.root)), qname=shlex.quote(name))
255
+
256
+ def _call(self, fn, *args) -> None:
257
+ try:
258
+ fn(*args)
259
+ except Exception:
260
+ print(f"[folderui:{self.root.name}] {fn.__name__}() 發生錯誤:", file=sys.stderr)
261
+ traceback.print_exc()
@@ -0,0 +1,140 @@
1
+ Metadata-Version: 2.5
2
+ Name: folderui
3
+ Version: 0.1.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
+ 把**資料夾變成立即式 GUI**,給人類和 AI agent 使用。資料夾裡只有兩種東西:
23
+
24
+ - `*.md` 等檔案:畫面(隨時可以 `cat`)
25
+ - `*.sh`:按鈕(只有現在能按的才會出現)
26
+
27
+ 執行按鈕後,腳本會**一直等到你需要做下一個決定**才回應,並印出:操作結果 + 這段期間發生的事 + 目前狀況 + 可以按的按鈕。
28
+ 所以 agent 的迴圈就是「執行按鈕 → 讀回應 → 執行下一個按鈕」,不需要另外等待或輪詢。
29
+
30
+ ```sh
31
+ pip install folderui # 或 uv add folderui;只用標準函式庫,需要 POSIX shell
32
+ ```
33
+
34
+ ```console
35
+ $ ./bid.sh 40
36
+ ⏳ 已送出,等待中… ← 其他人還在出價,腳本卡住
37
+ ✅ 已出價 40
38
+ 開標:bob 5、carol 15、你 40
39
+ 🔨 你以 40 得標 …
40
+ 🔔 bob 拍賣「月光琉璃盞」…
41
+
42
+ ── 可以做的事 ──
43
+ ./bid.sh <金額 1-60> 密封出價
44
+ ./pass.sh 不出價
45
+ ```
46
+
47
+ ## 寫一個 UI
48
+
49
+ ```python
50
+ from folderui import FolderUI
51
+
52
+ class Guess(FolderUI):
53
+ def draw(self): # 每一幀重新宣告畫面,不可修改狀態
54
+ self.file("rules.md", "猜 1-100")
55
+ self.button("guess", "<數字>")
56
+ self.reply(f"已經猜了 {len(self.tries)} 次")
57
+
58
+ def action(self, name, args): # 按鈕被按下:回傳文字,或 raise ValueError 代表失敗
59
+ n = int(args[0])
60
+ ...
61
+ return "太大"
62
+
63
+ Guess("/tmp/my-ui").run()
64
+ ```
65
+
66
+ | 方法 | 在哪裡呼叫 | 作用 |
67
+ |---|---|---|
68
+ | `file(path, content)` | `draw()` | 宣告一個檔案;沒宣告的會自動刪除 |
69
+ | `button(path, hint, options=None)` | `draw()` | 宣告一個按鈕 → `<path>.sh`,路徑可以分層(`players/2/kick`) |
70
+ | `reply(text)` | `draw()` | 腳本回應時印出的目前狀況 |
71
+ | `notify(text)` | `action()` / `receive()` | 累積一則消息,下一次回應時送出 |
72
+ | `close()` | `action()` / `receive()` | 結束:移除按鈕、回應等待中的腳本 |
73
+ | `push(msg)` | 其他執行緒 | 送訊息給這個 UI,會呼叫 `receive(msg)` |
74
+
75
+ **回應規則只有一條**:操作處理完之後,第一個「有按鈕可按」的畫面出現時(或 `close()`)才回應。
76
+
77
+ - 輸入錯誤 → 狀態沒變、按鈕還在 → 立刻回應(`raise` 的訊息,exit code 1)
78
+ - 查詢類操作 → 按鈕還在 → 立刻回應
79
+ - 送出後要等別人 → 沒有按鈕 → 等到輪到你
80
+
81
+ ## 多人:1 個伺服器 + N 個客戶端
82
+
83
+ ```python
84
+ class MyServer(Server):
85
+ def handle(self, who, cmd): # 純邏輯;raise ValueError 代表規則不允許
86
+ ...
87
+ for p in self.clients:
88
+ self.push(p, Update(...)) # 推播給客戶端
89
+ return "✅ 已出價"
90
+
91
+ class PlayerUI(FolderUI):
92
+ def action(self, name, args):
93
+ return self.server.call(self.me, ("bid", int(args[0]))) # 同步呼叫,例外會傳回來
94
+ def receive(self, update):
95
+ self.view = update.view # 伺服器推播 → 框架自動重畫
96
+
97
+ server = MyServer()
98
+ for i in range(n):
99
+ server.connect(i, PlayerUI(f"{root}/player_{i}", server, i))
100
+ server.start()
101
+ for ui in server.clients.values():
102
+ ui.start()
103
+ ```
104
+
105
+ 框架不管多人同步:伺服器在一個執行緒依序處理請求,推播走無上限佇列(伺服器永遠不等客戶端)。
106
+
107
+ ## 程式型 agent
108
+
109
+ ```python
110
+ from folderui import Session
111
+ s = Session("/tmp/folderui-auction-xxxx/player_0")
112
+ s.ready()
113
+ while buttons := s.buttons(): # {路徑: {hint, options}},空的代表結束
114
+ ok, text = s.press("bid", "40")
115
+ ```
116
+
117
+ ## 細節
118
+
119
+ - 腳本預設無限等待。設定 `FOLDERUI_TIMEOUT=秒數` 會在逾時後以 exit code 2 結束;
120
+ **再執行一次同樣的指令即可繼續等待或拿到回應**(不會重複送出)。回應被讀走之前,該按鈕的腳本會保留。
121
+ - 協定檔案都是隱藏檔:`.request`、`.response`、`.done`、`.buttons.json`。
122
+ 不能執行腳本的 agent 也可以直接寫 `.request`(按鈕路徑與參數以 NUL 分隔)。
123
+
124
+ ## 範例
125
+
126
+ ```sh
127
+ uv run python examples/guess.py # 單人:猜數字
128
+ uv run python examples/auction.py --players 4 --rounds 3 # 多人:英式拍賣(玩家 + 觀眾)
129
+ uv run python examples/auction_bot.py <資料夾> bot0 # 拍賣用的隨機 bot
130
+ ```
131
+
132
+ ## 開發
133
+
134
+ ```sh
135
+ uv sync
136
+ uv run pytest
137
+ ```
138
+
139
+ 發布:在 PyPI 設定 trusted publisher(repo `xiao-e-yun/folderui`、workflow `publish.yml`、environment `pypi`),
140
+ 之後改 `pyproject.toml` 的版本號並推送 `v*` tag 就會自動發布。
@@ -0,0 +1,10 @@
1
+ folderui/__init__.py,sha256=DgcWogffdAcswuQ4xnVbsra6JwV1PVDGyhHiTo6W9V4,206
2
+ folderui/fsutil.py,sha256=ddo13cW3NdwZQSr9y2vppC1xVnPlB1QVgzrKplS6Nog,799
3
+ folderui/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ folderui/server.py,sha256=_gEhObn4GtDpl5AJQHkbCEhm07MyAsW8HpZUNFoQNNk,2130
5
+ folderui/session.py,sha256=U-c0Pi1YAh6_eSXONoUPNsaO4UIi0FxvT2ORzN6dgHA,1943
6
+ folderui/ui.py,sha256=hGJ2BLCdTYqdd0bDFCL7L2J4AJnVBvTilpTR2T17uSU,9723
7
+ folderui-0.1.0.dist-info/METADATA,sha256=lH5hDlDZU3QlQQD3fdWydJ9kwy5O-zCez1HuFR3aW1c,5361
8
+ folderui-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
9
+ folderui-0.1.0.dist-info/licenses/LICENSE,sha256=tcJlcDzyGAbI59TkI8E2ePZroQpo4WJyXpSq_Bqa-PI,1067
10
+ folderui-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 xiao-e-yun
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.