ccoding 0.2.1__tar.gz → 0.2.2__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.
Files changed (33) hide show
  1. {ccoding-0.2.1 → ccoding-0.2.2}/PKG-INFO +15 -1
  2. {ccoding-0.2.1 → ccoding-0.2.2}/README.md +14 -0
  3. {ccoding-0.2.1 → ccoding-0.2.2}/pyproject.toml +1 -1
  4. ccoding-0.2.2/screenshot_mobile1.jpeg +0 -0
  5. ccoding-0.2.2/screenshot_mobile2.jpeg +0 -0
  6. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/web/app.js +176 -10
  7. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/web/index.html +17 -1
  8. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/web/style.css +102 -0
  9. {ccoding-0.2.1 → ccoding-0.2.2}/.github/workflows/publish.yml +0 -0
  10. {ccoding-0.2.1 → ccoding-0.2.2}/.gitignore +0 -0
  11. {ccoding-0.2.1 → ccoding-0.2.2}/CLAUDE.md +0 -0
  12. {ccoding-0.2.1 → ccoding-0.2.2}/LICENSE +0 -0
  13. {ccoding-0.2.1 → ccoding-0.2.2}/screenshot.png +0 -0
  14. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/__init__.py +0 -0
  15. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/__main__.py +0 -0
  16. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/api.py +0 -0
  17. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/cli.py +0 -0
  18. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/config.py +0 -0
  19. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/env_schema.json +0 -0
  20. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/logo.py +0 -0
  21. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/remote.py +0 -0
  22. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/remote_cli.py +0 -0
  23. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/remote_client.py +0 -0
  24. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/remote_common.py +0 -0
  25. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/remote_server.py +0 -0
  26. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/ui.py +0 -0
  27. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/web/__init__.py +0 -0
  28. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/web/assets/addon-fit.js +0 -0
  29. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/web/assets/xterm-LICENSE +0 -0
  30. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/web/assets/xterm-addon-fit-LICENSE +0 -0
  31. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/web/assets/xterm.css +0 -0
  32. {ccoding-0.2.1 → ccoding-0.2.2}/src/ccode/web/assets/xterm.js +0 -0
  33. {ccoding-0.2.1 → ccoding-0.2.2}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ccoding
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Curses-based Claude Code launcher
5
5
  Project-URL: Homepage, https://github.com/8DE4732A/ccode
6
6
  Project-URL: Repository, https://github.com/8DE4732A/ccode
@@ -57,6 +57,11 @@ Description-Content-Type: text/markdown
57
57
 
58
58
  ![](screenshot.png)
59
59
 
60
+ <p align="center">
61
+ <img src="screenshot_mobile1.jpeg" alt="Mobile remote terminal screenshot 1" width="45%" />
62
+ <img src="screenshot_mobile2.jpeg" alt="Mobile remote terminal screenshot 2" width="45%" />
63
+ </p>
64
+
60
65
  ## 功能特点
61
66
 
62
67
  - 终端 curses UI,键盘全操作
@@ -127,6 +132,15 @@ ccode-remote # 查看 remote hub / client connector 状态
127
132
 
128
133
  需要 Python >=3.10。基础 TUI 不再强制安装 remote 依赖。Remote/Web 模式需要额外依赖,并需要系统中已安装 `tmux` 和 `claude`;Windows 建议在 WSL 中使用 remote 模式。
129
134
 
135
+ 使用 Remote/Web 时,推荐在 `~/.tmux.conf` 中配置:
136
+
137
+ ```tmux
138
+ set -g history-limit 50000
139
+ set -g mouse on
140
+ set -g default-terminal "screen-256color"
141
+ set-option -ga terminal-overrides ",xterm-256color:Tc"
142
+ ```
143
+
130
144
  ```bash
131
145
  git clone https://github.com/8DE4732A/ccode
132
146
  cd ccode
@@ -6,6 +6,11 @@
6
6
 
7
7
  ![](screenshot.png)
8
8
 
9
+ <p align="center">
10
+ <img src="screenshot_mobile1.jpeg" alt="Mobile remote terminal screenshot 1" width="45%" />
11
+ <img src="screenshot_mobile2.jpeg" alt="Mobile remote terminal screenshot 2" width="45%" />
12
+ </p>
13
+
9
14
  ## 功能特点
10
15
 
11
16
  - 终端 curses UI,键盘全操作
@@ -76,6 +81,15 @@ ccode-remote # 查看 remote hub / client connector 状态
76
81
 
77
82
  需要 Python >=3.10。基础 TUI 不再强制安装 remote 依赖。Remote/Web 模式需要额外依赖,并需要系统中已安装 `tmux` 和 `claude`;Windows 建议在 WSL 中使用 remote 模式。
78
83
 
84
+ 使用 Remote/Web 时,推荐在 `~/.tmux.conf` 中配置:
85
+
86
+ ```tmux
87
+ set -g history-limit 50000
88
+ set -g mouse on
89
+ set -g default-terminal "screen-256color"
90
+ set-option -ga terminal-overrides ",xterm-256color:Tc"
91
+ ```
92
+
79
93
  ```bash
80
94
  git clone https://github.com/8DE4732A/ccode
81
95
  cd ccode
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ccoding"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Curses-based Claude Code launcher"
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
Binary file
Binary file
@@ -3,23 +3,127 @@ const sessionListEl = document.getElementById("session-list");
3
3
  const tokenFormEl = document.getElementById("token-form");
4
4
  const tokenInputEl = document.getElementById("token-input");
5
5
  const statusEl = document.getElementById("status");
6
+ const actionsEl = document.getElementById("actions");
7
+ const actionsToggleButton = document.getElementById("actions-toggle");
6
8
  const backButton = document.getElementById("back");
7
9
  const refreshButton = document.getElementById("refresh");
8
10
  const changeTokenButton = document.getElementById("change-token");
9
11
  const reconnectButton = document.getElementById("reconnect");
10
12
  const focusButton = document.getElementById("focus");
13
+ const mobileKeybarEl = document.getElementById("mobile-keybar");
14
+
15
+ const inputSequences = {
16
+ tab: "\t",
17
+ esc: "\x1b",
18
+ "ctrl-c": "\x03",
19
+ "ctrl-d": "\x04",
20
+ "ctrl-l": "\x0c",
21
+ enter: "\r",
22
+ backspace: "\x7f",
23
+ up: "\x1b[A",
24
+ down: "\x1b[B",
25
+ right: "\x1b[C",
26
+ left: "\x1b[D",
27
+ };
11
28
 
12
29
  const tokenStorageKey = "ccode.remote.adminToken";
30
+ const fontSizeStorageKey = "ccode.remote.terminalFontSize";
13
31
  let token = window.sessionStorage.getItem(tokenStorageKey) || "";
14
32
  let socket = null;
15
33
  let term = null;
16
34
  let fitAddon = null;
17
35
  let currentSession = null;
36
+ let terminalFontSize = Number(window.localStorage.getItem(fontSizeStorageKey)) || 14;
37
+ let terminalTouchScroll = null;
38
+ let terminalViewportEl = null;
39
+ let resizeFrame = null;
40
+ let viewportHeight = "";
41
+ let keyboardInset = "";
18
42
 
19
43
  function setStatus(message) {
20
44
  statusEl.textContent = message;
21
45
  }
22
46
 
47
+ function sendInput(data) {
48
+ if (!socket || socket.readyState !== WebSocket.OPEN) {
49
+ return;
50
+ }
51
+ socket.send(JSON.stringify({ type: "input", data }));
52
+ }
53
+
54
+ function focusTerminal() {
55
+ ensureTerminal();
56
+ term.focus();
57
+ }
58
+
59
+ function adjustTerminalFontSize(delta) {
60
+ const nextFontSize = Math.min(22, Math.max(10, terminalFontSize + delta));
61
+ if (nextFontSize === terminalFontSize) {
62
+ return;
63
+ }
64
+ terminalFontSize = nextFontSize;
65
+ window.localStorage.setItem(fontSizeStorageKey, String(terminalFontSize));
66
+ if (term) {
67
+ term.options.fontSize = terminalFontSize;
68
+ fitAndSendResize();
69
+ }
70
+ }
71
+
72
+ function closeActionMenu() {
73
+ actionsEl.classList.remove("open");
74
+ actionsToggleButton.setAttribute("aria-expanded", "false");
75
+ }
76
+
77
+ function beginTerminalTouchScroll(y) {
78
+ if (!term || terminalTouchScroll) {
79
+ return;
80
+ }
81
+ terminalTouchScroll = { y, remainder: 0, active: false };
82
+ }
83
+
84
+ function scrollTerminalFromTouch(y, event) {
85
+ if (!terminalTouchScroll || !term) {
86
+ return;
87
+ }
88
+ const deltaY = terminalTouchScroll.y - y;
89
+ if (!terminalTouchScroll.active && Math.abs(deltaY) < 8) {
90
+ return;
91
+ }
92
+ terminalTouchScroll.active = true;
93
+ event.preventDefault();
94
+ if (terminalViewportEl) {
95
+ terminalViewportEl.dispatchEvent(new WheelEvent("wheel", { deltaY, bubbles: true, cancelable: true }));
96
+ }
97
+ const lineHeight = Math.max(14, terminalFontSize * 1.35);
98
+ const delta = deltaY + terminalTouchScroll.remainder;
99
+ const lines = Math.trunc(delta / lineHeight);
100
+ terminalTouchScroll.remainder = delta - lines * lineHeight;
101
+ terminalTouchScroll.y = y;
102
+ if (lines) {
103
+ term.scrollLines(lines);
104
+ }
105
+ }
106
+
107
+ function endTerminalTouchScroll() {
108
+ terminalTouchScroll = null;
109
+ }
110
+
111
+ function startTerminalTouchScroll(event) {
112
+ if (event.touches.length === 1) {
113
+ beginTerminalTouchScroll(event.touches[0].clientY);
114
+ }
115
+ }
116
+
117
+ function moveTerminalTouchScroll(event) {
118
+ if (event.touches.length === 1) {
119
+ scrollTerminalFromTouch(event.touches[0].clientY, event);
120
+ }
121
+ }
122
+
123
+ function stopTerminalTouchScroll() {
124
+ endTerminalTouchScroll();
125
+ }
126
+
23
127
  function ensureTerminal() {
24
128
  if (term) {
25
129
  return;
@@ -28,7 +132,7 @@ function ensureTerminal() {
28
132
  cursorBlink: true,
29
133
  convertEol: true,
30
134
  fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
31
- fontSize: 14,
135
+ fontSize: terminalFontSize,
32
136
  theme: {
33
137
  background: "#05070a",
34
138
  foreground: "#d6e2f0",
@@ -39,15 +143,36 @@ function ensureTerminal() {
39
143
  fitAddon = new FitAddon.FitAddon();
40
144
  term.loadAddon(fitAddon);
41
145
  term.open(terminalEl);
42
- term.onData((data) => {
43
- if (!socket || socket.readyState !== WebSocket.OPEN) {
44
- return;
45
- }
46
- socket.send(JSON.stringify({ type: "input", data }));
146
+ terminalViewportEl = terminalEl.querySelector(".xterm-viewport");
147
+ term.onData(sendInput);
148
+ }
149
+
150
+ function updateViewportInsets() {
151
+ const viewport = window.visualViewport;
152
+ const nextViewportHeight = `${viewport ? viewport.height : window.innerHeight}px`;
153
+ const nextKeyboardInset = `${viewport ? Math.max(0, window.innerHeight - viewport.height - viewport.offsetTop) : 0}px`;
154
+ if (nextViewportHeight !== viewportHeight) {
155
+ viewportHeight = nextViewportHeight;
156
+ document.documentElement.style.setProperty("--viewport-height", viewportHeight);
157
+ }
158
+ if (nextKeyboardInset !== keyboardInset) {
159
+ keyboardInset = nextKeyboardInset;
160
+ document.documentElement.style.setProperty("--keyboard-inset", keyboardInset);
161
+ }
162
+ }
163
+
164
+ function requestFitAndSendResize() {
165
+ if (resizeFrame !== null) {
166
+ return;
167
+ }
168
+ resizeFrame = window.requestAnimationFrame(() => {
169
+ resizeFrame = null;
170
+ fitAndSendResize();
47
171
  });
48
172
  }
49
173
 
50
174
  function fitAndSendResize() {
175
+ updateViewportInsets();
51
176
  if (!term || !fitAddon || terminalEl.hidden) {
52
177
  return;
53
178
  }
@@ -71,9 +196,12 @@ function showTokenMode(message = "enter token") {
71
196
  terminalEl.hidden = true;
72
197
  sessionListEl.hidden = true;
73
198
  tokenFormEl.hidden = false;
199
+ actionsToggleButton.hidden = true;
200
+ closeActionMenu();
74
201
  backButton.hidden = true;
75
202
  focusButton.hidden = true;
76
203
  reconnectButton.hidden = true;
204
+ mobileKeybarEl.hidden = true;
77
205
  refreshButton.hidden = true;
78
206
  changeTokenButton.hidden = true;
79
207
  setStatus(message);
@@ -87,9 +215,12 @@ function showListMode() {
87
215
  tokenFormEl.hidden = true;
88
216
  terminalEl.hidden = true;
89
217
  sessionListEl.hidden = false;
218
+ actionsToggleButton.hidden = false;
219
+ closeActionMenu();
90
220
  backButton.hidden = true;
91
221
  focusButton.hidden = true;
92
222
  reconnectButton.hidden = true;
223
+ mobileKeybarEl.hidden = true;
93
224
  refreshButton.hidden = false;
94
225
  changeTokenButton.hidden = false;
95
226
  }
@@ -98,9 +229,12 @@ function showTerminalMode() {
98
229
  tokenFormEl.hidden = true;
99
230
  sessionListEl.hidden = true;
100
231
  terminalEl.hidden = false;
232
+ actionsToggleButton.hidden = false;
233
+ closeActionMenu();
101
234
  backButton.hidden = false;
102
235
  focusButton.hidden = false;
103
236
  reconnectButton.hidden = false;
237
+ mobileKeybarEl.hidden = false;
104
238
  refreshButton.hidden = true;
105
239
  changeTokenButton.hidden = false;
106
240
  }
@@ -276,13 +410,45 @@ changeTokenButton.addEventListener("click", () => {
276
410
  token = "";
277
411
  showTokenMode("enter token");
278
412
  });
279
- window.addEventListener("resize", fitAndSendResize);
413
+ window.addEventListener("resize", requestFitAndSendResize);
414
+ actionsToggleButton.addEventListener("click", () => {
415
+ const open = actionsEl.classList.toggle("open");
416
+ actionsToggleButton.setAttribute("aria-expanded", String(open));
417
+ });
418
+ actionsEl.addEventListener("click", (event) => {
419
+ if (event.target.closest("button")) {
420
+ closeActionMenu();
421
+ }
422
+ });
280
423
  backButton.addEventListener("click", loadSessions);
281
424
  refreshButton.addEventListener("click", loadSessions);
282
425
  reconnectButton.addEventListener("click", connect);
283
- focusButton.addEventListener("click", () => {
284
- ensureTerminal();
285
- term.focus();
426
+ focusButton.addEventListener("click", focusTerminal);
427
+ terminalEl.addEventListener("touchstart", startTerminalTouchScroll, { passive: true });
428
+ terminalEl.addEventListener("touchmove", moveTerminalTouchScroll, { passive: false });
429
+ terminalEl.addEventListener("touchend", stopTerminalTouchScroll);
430
+ terminalEl.addEventListener("touchcancel", stopTerminalTouchScroll);
431
+ mobileKeybarEl.addEventListener("pointerdown", (event) => {
432
+ const inputButton = event.target.closest("button[data-sequence]");
433
+ const zoomButton = event.target.closest("button[data-zoom]");
434
+ if (!inputButton && !zoomButton) {
435
+ return;
436
+ }
437
+ event.preventDefault();
438
+ if (zoomButton) {
439
+ adjustTerminalFontSize(Number(zoomButton.dataset.zoom));
440
+ return;
441
+ }
442
+ const data = inputSequences[inputButton.dataset.sequence];
443
+ if (data) {
444
+ sendInput(data);
445
+ focusTerminal();
446
+ }
286
447
  });
448
+ if (window.visualViewport) {
449
+ window.visualViewport.addEventListener("resize", requestFitAndSendResize);
450
+ window.visualViewport.addEventListener("scroll", requestFitAndSendResize);
451
+ }
452
+ updateViewportInsets();
287
453
 
288
454
  loadSessions();
@@ -14,7 +14,8 @@
14
14
  <h1>ccode remote</h1>
15
15
  <p id="status">enter token</p>
16
16
  </div>
17
- <div class="actions">
17
+ <button id="actions-toggle" class="actions-toggle" type="button" hidden>Menu</button>
18
+ <div id="actions" class="actions">
18
19
  <button id="back" type="button" hidden>Sessions</button>
19
20
  <button id="refresh" type="button" hidden>Refresh</button>
20
21
  <button id="change-token" type="button" hidden>Change token</button>
@@ -31,6 +32,21 @@
31
32
  </form>
32
33
  <section id="session-list" hidden></section>
33
34
  <div id="terminal" tabindex="0" hidden></div>
35
+ <div id="mobile-keybar" class="mobile-keybar" hidden>
36
+ <button type="button" data-zoom="-1">A−</button>
37
+ <button type="button" data-zoom="1">A+</button>
38
+ <button type="button" data-sequence="tab">Tab</button>
39
+ <button type="button" data-sequence="esc">Esc</button>
40
+ <button type="button" data-sequence="ctrl-c">Ctrl+C</button>
41
+ <button type="button" data-sequence="ctrl-d">Ctrl+D</button>
42
+ <button type="button" data-sequence="ctrl-l">Ctrl+L</button>
43
+ <button type="button" data-sequence="enter">Enter</button>
44
+ <button type="button" data-sequence="backspace">⌫</button>
45
+ <button type="button" data-sequence="up">↑</button>
46
+ <button type="button" data-sequence="down">↓</button>
47
+ <button type="button" data-sequence="left">←</button>
48
+ <button type="button" data-sequence="right">→</button>
49
+ </div>
34
50
  </main>
35
51
  <script src="assets/xterm.js"></script>
36
52
  <script src="assets/addon-fit.js"></script>
@@ -10,6 +10,11 @@ main {
10
10
  margin: 0;
11
11
  }
12
12
 
13
+ :root {
14
+ --keyboard-inset: 0px;
15
+ --viewport-height: 100vh;
16
+ }
17
+
13
18
  body {
14
19
  background: #090b10;
15
20
  color: #e8eef8;
@@ -47,6 +52,10 @@ p {
47
52
  justify-content: flex-end;
48
53
  }
49
54
 
55
+ .actions-toggle {
56
+ display: none;
57
+ }
58
+
50
59
  button,
51
60
  input {
52
61
  border-radius: 8px;
@@ -168,12 +177,105 @@ button[hidden],
168
177
  min-height: 0;
169
178
  overflow: hidden;
170
179
  padding: 12px;
180
+ touch-action: none;
171
181
  }
172
182
 
173
183
  #terminal:focus-within {
174
184
  outline: 2px solid #4f8cff;
175
185
  }
176
186
 
187
+ .mobile-keybar {
188
+ display: none;
189
+ }
190
+
177
191
  .xterm {
178
192
  height: 100%;
179
193
  }
194
+
195
+ @media (max-width: 720px), (pointer: coarse) {
196
+ html,
197
+ body,
198
+ main {
199
+ height: var(--viewport-height);
200
+ overflow: hidden;
201
+ }
202
+
203
+ main {
204
+ padding: 8px;
205
+ gap: 8px;
206
+ }
207
+
208
+ header {
209
+ align-items: flex-start;
210
+ gap: 8px;
211
+ position: relative;
212
+ }
213
+
214
+ .actions-toggle:not([hidden]) {
215
+ display: block;
216
+ margin-left: auto;
217
+ }
218
+
219
+ .actions {
220
+ background: rgba(9, 11, 16, 0.98);
221
+ border: 1px solid #1c2638;
222
+ border-radius: 12px;
223
+ box-shadow: 0 14px 40px rgba(0, 0, 0, 0.38);
224
+ display: none;
225
+ flex-direction: column;
226
+ min-width: 180px;
227
+ padding: 8px;
228
+ position: absolute;
229
+ right: 0;
230
+ top: calc(100% + 6px);
231
+ z-index: 20;
232
+ }
233
+
234
+ .actions.open {
235
+ display: flex;
236
+ }
237
+
238
+ .actions button {
239
+ text-align: left;
240
+ width: 100%;
241
+ }
242
+
243
+ #session-list {
244
+ overflow: auto;
245
+ }
246
+
247
+ #terminal {
248
+ margin-bottom: 58px;
249
+ min-height: 120px;
250
+ padding: 8px;
251
+ }
252
+
253
+ .mobile-keybar:not([hidden]) {
254
+ background: rgba(9, 11, 16, 0.96);
255
+ border: 1px solid #1c2638;
256
+ border-radius: 12px;
257
+ bottom: calc(8px + var(--keyboard-inset));
258
+ display: flex;
259
+ gap: 6px;
260
+ left: 8px;
261
+ max-width: calc(100vw - 16px);
262
+ overflow-x: auto;
263
+ padding: 8px;
264
+ position: fixed;
265
+ right: 8px;
266
+ scrollbar-width: none;
267
+ z-index: 10;
268
+ }
269
+
270
+ .mobile-keybar::-webkit-scrollbar {
271
+ display: none;
272
+ }
273
+
274
+ .mobile-keybar button {
275
+ flex: 0 0 auto;
276
+ min-width: 48px;
277
+ padding: 10px 12px;
278
+ touch-action: manipulation;
279
+ user-select: none;
280
+ }
281
+ }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes