feat: Nerd Font support, clipboard paste, and PTY environment fixes
- Add ttyd-nerd-font submodule with embedded JetBrains Mono Nerd Font for proper icon rendering in browser via ttyd - Add Ctrl+Shift+V / Shift+Insert paste support in ttyd frontend - Strip Zellij env vars from PTY child processes to prevent workspace terminal from inheriting Zellij session context - Launch PTY commands through login shell for proper PATH resolution - Use /etc/passwd shell lookup instead of $SHELL (Zellij overrides it) - Soften ANSI 256-color palette to match modern dark themes - Handle Nerd Font PUA glyphs as width 1 in pyte terminal emulator - Fix brightmagenta typo in pyte graphics - Update install script to build ttyd from submodule source - Add web deployment TODO items for image paste and context menu Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[submodule "deploy/ttyd-nerd-font"]
|
||||
path = deploy/ttyd-nerd-font
|
||||
url = https://github.com/metorm/ttyd-nerd-font.git
|
||||
@@ -110,6 +110,11 @@ Long-term open items for Clide development.
|
||||
- [ ] Plugin development guide
|
||||
- [ ] Architecture documentation updates
|
||||
|
||||
## Web Deployment (ttyd)
|
||||
|
||||
- [ ] Image paste support: intercept browser clipboard image on Ctrl+V, upload blob to server, save as temp file. Claude Code already handles Ctrl+V as image paste — just needs the image on the filesystem
|
||||
- [ ] Right-click context menu for copy/paste in web terminal
|
||||
|
||||
## Build & Distribution
|
||||
|
||||
- [ ] PyInstaller builds for macOS
|
||||
|
||||
+26
-1
@@ -192,7 +192,7 @@ class ClideApp(App[None]):
|
||||
test_mode: bool = False,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.workdir = workdir or Path.cwd()
|
||||
self.workdir = self._resolve_workdir(workdir)
|
||||
self.settings = settings or ClideSettings()
|
||||
self._test_mode = test_mode
|
||||
|
||||
@@ -221,6 +221,31 @@ class ClideApp(App[None]):
|
||||
# Register additional syntax highlighting languages
|
||||
register_languages()
|
||||
|
||||
@staticmethod
|
||||
def _resolve_workdir(workdir: Path | None) -> Path:
|
||||
"""Resolve the working directory to the git root when possible."""
|
||||
import subprocess
|
||||
|
||||
target = workdir.resolve() if workdir and workdir.is_dir() else Path.cwd()
|
||||
|
||||
# Find git root -- anchors to the project root even if cwd is a subdirectory
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
cwd=str(target),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=3,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
git_root = Path(result.stdout.strip())
|
||||
if git_root.is_dir():
|
||||
return git_root
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
||||
pass
|
||||
|
||||
return target
|
||||
|
||||
def _register_themes(self) -> None:
|
||||
"""Register all themes with Textual."""
|
||||
for theme_meta in get_all_themes():
|
||||
|
||||
Vendored
+18
-18
@@ -99,7 +99,7 @@ BG_AIXTERM = {
|
||||
102: "brightgreen",
|
||||
103: "brightbrown",
|
||||
104: "brightblue",
|
||||
105: "bfightmagenta",
|
||||
105: "brightmagenta",
|
||||
106: "brightcyan",
|
||||
107: "brightwhite",
|
||||
}
|
||||
@@ -111,24 +111,24 @@ FG_256 = 38
|
||||
BG_256 = 48
|
||||
|
||||
#: A table of 256 foreground or background colors.
|
||||
# The following code is part of the Pygments project (BSD licensed).
|
||||
#: First 16 entries softened from xterm defaults to match modern terminal themes.
|
||||
_FG_BG_256 = [
|
||||
(0x00, 0x00, 0x00), # 0
|
||||
(0xCD, 0x00, 0x00), # 1
|
||||
(0x00, 0xCD, 0x00), # 2
|
||||
(0xCD, 0xCD, 0x00), # 3
|
||||
(0x00, 0x00, 0xEE), # 4
|
||||
(0xCD, 0x00, 0xCD), # 5
|
||||
(0x00, 0xCD, 0xCD), # 6
|
||||
(0xE5, 0xE5, 0xE5), # 7
|
||||
(0x7F, 0x7F, 0x7F), # 8
|
||||
(0xFF, 0x00, 0x00), # 9
|
||||
(0x00, 0xFF, 0x00), # 10
|
||||
(0xFF, 0xFF, 0x00), # 11
|
||||
(0x5C, 0x5C, 0xFF), # 12
|
||||
(0xFF, 0x00, 0xFF), # 13
|
||||
(0x00, 0xFF, 0xFF), # 14
|
||||
(0xFF, 0xFF, 0xFF), # 15
|
||||
(0x28, 0x2C, 0x34), # 0 black
|
||||
(0xE0, 0x6C, 0x75), # 1 red
|
||||
(0x98, 0xC3, 0x79), # 2 green
|
||||
(0xE5, 0xC0, 0x7B), # 3 yellow
|
||||
(0x61, 0xAF, 0xEF), # 4 blue
|
||||
(0xC6, 0x78, 0xDD), # 5 magenta
|
||||
(0x56, 0xB6, 0xC2), # 6 cyan
|
||||
(0xAB, 0xB2, 0xBF), # 7 white
|
||||
(0x5C, 0x63, 0x70), # 8 bright black
|
||||
(0xE0, 0x6C, 0x75), # 9 bright red
|
||||
(0x98, 0xC3, 0x79), # 10 bright green
|
||||
(0xE5, 0xC0, 0x7B), # 11 bright yellow
|
||||
(0x61, 0xAF, 0xEF), # 12 bright blue
|
||||
(0xC6, 0x78, 0xDD), # 13 bright magenta
|
||||
(0x56, 0xB6, 0xC2), # 14 bright cyan
|
||||
(0xFF, 0xFF, 0xFF), # 15 bright white
|
||||
]
|
||||
|
||||
# colors 16..231: the 6x6x6 color cube
|
||||
|
||||
Vendored
+17
-1
@@ -310,6 +310,20 @@ class Screen:
|
||||
for char in data:
|
||||
char_width = wcwidth(char)
|
||||
|
||||
# Treat Private Use Area and other nerd font glyphs as width 1.
|
||||
# wcwidth returns -1 for PUA chars (U+E000-U+F8FF, U+F0000-U+FFFFF)
|
||||
# which are used by Nerd Fonts for icons in tmux status bars, etc.
|
||||
if char_width < 0:
|
||||
code = ord(char)
|
||||
if (0xE000 <= code <= 0xF8FF or # BMP Private Use Area
|
||||
0xF0000 <= code <= 0xFFFFF or # Supplementary PUA-A
|
||||
0x100000 <= code <= 0x10FFFF or # Supplementary PUA-B
|
||||
0x2580 <= code <= 0x259F or # Block Elements
|
||||
0x1F000 <= code <= 0x1FFFF): # Symbols/Emoji
|
||||
char_width = 1
|
||||
else:
|
||||
continue # Skip truly unprintable chars instead of breaking
|
||||
|
||||
# Clide: Log character drawing for debugging
|
||||
if _debug_logger is not None and char_width > 0:
|
||||
code = ord(char)
|
||||
@@ -347,8 +361,10 @@ class Screen:
|
||||
self.buffer[self.cursor.y - 1][self.columns - 1] = last._replace(
|
||||
data=normalized
|
||||
)
|
||||
elif char_width == 0:
|
||||
continue # Skip zero-width non-combining chars
|
||||
else:
|
||||
break
|
||||
continue # Skip any remaining unhandled chars
|
||||
|
||||
if char_width > 0:
|
||||
self.cursor.x = min(self.cursor.x + char_width, self.columns)
|
||||
|
||||
@@ -181,11 +181,29 @@ class TerminalDisplay(Widget, can_focus=True):
|
||||
if pid == 0:
|
||||
# Child process
|
||||
os.chdir(cwd)
|
||||
|
||||
# Strip Zellij env vars so child processes don't think
|
||||
# they're inside a Zellij session (Clide may run inside
|
||||
# Zellij for web deployment session persistence)
|
||||
for key in list(os.environ):
|
||||
if key.startswith("ZELLIJ"):
|
||||
del os.environ[key]
|
||||
|
||||
os.environ["TERM"] = "xterm-256color"
|
||||
os.environ["COLORTERM"] = "truecolor"
|
||||
os.environ["COLUMNS"] = str(self._cols)
|
||||
os.environ["LINES"] = str(self._rows)
|
||||
os.execlp(command, command)
|
||||
# Launch through a login shell so profile scripts are sourced
|
||||
# and PATH includes locations like ~/.local/bin where Claude's
|
||||
# native binary may be installed.
|
||||
# Use /etc/passwd shell, not $SHELL (which Zellij may override)
|
||||
import pwd
|
||||
|
||||
try:
|
||||
shell = pwd.getpwuid(os.getuid()).pw_shell
|
||||
except KeyError:
|
||||
shell = "/bin/bash"
|
||||
os.execvp(shell, [shell, "-l", "-c", command])
|
||||
else:
|
||||
# Parent process
|
||||
self._pid = pid
|
||||
@@ -321,23 +339,25 @@ class TerminalDisplay(Widget, can_focus=True):
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# ANSI 256-color palette (standard 16 colors)
|
||||
# ANSI 256-color palette (standard 16 colors, softened for dark themes)
|
||||
ANSI_COLORS = {
|
||||
"black": "#000000",
|
||||
"red": "#cd0000",
|
||||
"green": "#00cd00",
|
||||
"yellow": "#cdcd00",
|
||||
"blue": "#0000ee",
|
||||
"magenta": "#cd00cd",
|
||||
"cyan": "#00cdcd",
|
||||
"white": "#e5e5e5",
|
||||
"brightblack": "#7f7f7f",
|
||||
"brightred": "#ff0000",
|
||||
"brightgreen": "#00ff00",
|
||||
"brightyellow": "#ffff00",
|
||||
"brightblue": "#5c5cff",
|
||||
"brightmagenta": "#ff00ff",
|
||||
"brightcyan": "#00ffff",
|
||||
"black": "#282c34",
|
||||
"red": "#e06c75",
|
||||
"green": "#98c379",
|
||||
"brown": "#e5c07b",
|
||||
"yellow": "#e5c07b",
|
||||
"blue": "#61afef",
|
||||
"magenta": "#c678dd",
|
||||
"cyan": "#56b6c2",
|
||||
"white": "#abb2bf",
|
||||
"brightblack": "#5c6370",
|
||||
"brightred": "#e06c75",
|
||||
"brightgreen": "#98c379",
|
||||
"brightbrown": "#e5c07b",
|
||||
"brightyellow": "#e5c07b",
|
||||
"brightblue": "#61afef",
|
||||
"brightmagenta": "#c678dd",
|
||||
"brightcyan": "#56b6c2",
|
||||
"brightwhite": "#ffffff",
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,20 @@ class TerminalPane(Vertical):
|
||||
super().__init__(**kwargs)
|
||||
self._cwd = cwd or Path.cwd()
|
||||
self._terminal: TerminalDisplay | None = None
|
||||
self._shell = os.environ.get("SHELL", "/bin/bash")
|
||||
self._shell = self._find_shell()
|
||||
|
||||
@staticmethod
|
||||
def _find_shell() -> str:
|
||||
"""Find the user's real shell, ignoring Zellij overrides."""
|
||||
import pwd
|
||||
|
||||
# Get shell from /etc/passwd (most reliable)
|
||||
try:
|
||||
return pwd.getpwuid(os.getuid()).pw_shell
|
||||
except KeyError:
|
||||
pass
|
||||
# Fallback
|
||||
return "/bin/bash"
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
yield Static(f"Terminal - {self._cwd}", classes="terminal-header")
|
||||
|
||||
@@ -32,6 +32,7 @@ class ClaudePanel(Vertical):
|
||||
|
||||
ClaudePanel.with-workspace {
|
||||
height: 40%;
|
||||
border-top: solid $surface;
|
||||
}
|
||||
|
||||
ClaudePanel TerminalDisplay {
|
||||
|
||||
+18
-7
@@ -57,12 +57,23 @@ fi
|
||||
|
||||
SESSION_NAME="clide-$PROJECT"
|
||||
|
||||
# Check if session exists (live or dead)
|
||||
cd "$WORKDIR"
|
||||
if zellij list-sessions 2>/dev/null | grep -q "$SESSION_NAME"; then
|
||||
# Attach to existing session (resurrects dead sessions automatically)
|
||||
exec zellij attach "$SESSION_NAME"
|
||||
# Clean up dead/exited zellij sessions for this project
|
||||
if zellij list-sessions 2>/dev/null | grep -q "$SESSION_NAME.*EXITED"; then
|
||||
zellij delete-session "$SESSION_NAME" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Try to attach to a live session, otherwise create fresh
|
||||
if zellij list-sessions 2>/dev/null | grep -q "^$SESSION_NAME "; then
|
||||
# Session exists and is alive -- attach
|
||||
exec zellij attach "$SESSION_NAME" \
|
||||
options --no-pane-frames --default-cwd "$WORKDIR"
|
||||
else
|
||||
# Create new session
|
||||
exec zellij --session "$SESSION_NAME" options --default-shell "$CLIDE_BIN" --default-cwd "$WORKDIR"
|
||||
# No live session -- kill any zombie remnants and start fresh
|
||||
zellij delete-session "$SESSION_NAME" 2>/dev/null || true
|
||||
|
||||
exec zellij --session "$SESSION_NAME" \
|
||||
options \
|
||||
--no-pane-frames \
|
||||
--default-shell "$CLIDE_BIN" \
|
||||
--default-cwd "$WORKDIR"
|
||||
fi
|
||||
|
||||
@@ -13,7 +13,8 @@ WorkingDirectory=/mnt/media/Projects
|
||||
# -W : Writable (allows input)
|
||||
# -a : Allow URL arguments (passes ?project=X as command arg)
|
||||
# -t fontSize=14: Terminal font size
|
||||
ExecStart=/usr/local/bin/ttyd -p 8888 -W -a -t fontSize=14 /usr/local/bin/clide-launcher
|
||||
# -t fontFamily: Use embedded JetBrains Mono Nerd Font (from ttyd-nerd-font fork)
|
||||
ExecStart=/usr/local/bin/ttyd -p 8888 -W -a -t fontSize=14 -t 'fontFamily=JetBrains,monospace' /usr/local/bin/clide-launcher
|
||||
|
||||
# Kill all child processes (ttyd → zellij → clide → claude)
|
||||
KillMode=control-group
|
||||
|
||||
@@ -16,9 +16,20 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SUDO_USER="${SUDO_USER:-$(logname)}"
|
||||
USER_HOME=$(eval echo ~"$SUDO_USER")
|
||||
|
||||
echo "[1/6] Installing ttyd..."
|
||||
curl -L https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64 -o /usr/local/bin/ttyd
|
||||
echo "[1/6] Building ttyd (with Nerd Font support)..."
|
||||
TTYD_SRC="$SCRIPT_DIR/ttyd-nerd-font"
|
||||
if [[ ! -d "$TTYD_SRC/src" ]]; then
|
||||
echo "Error: ttyd-nerd-font submodule not initialized."
|
||||
echo "Run: git submodule update --init deploy/ttyd-nerd-font"
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$TTYD_SRC/build"
|
||||
cd "$TTYD_SRC/build"
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release >/dev/null 2>&1
|
||||
make -j"$(nproc)" >/dev/null 2>&1
|
||||
cp "$TTYD_SRC/build/ttyd" /usr/local/bin/ttyd
|
||||
chmod +x /usr/local/bin/ttyd
|
||||
cd "$SCRIPT_DIR"
|
||||
ttyd --version
|
||||
|
||||
echo ""
|
||||
|
||||
Submodule
+1
Submodule deploy/ttyd-nerd-font added at 8a6e952254
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
# Temporary script to install ttyd-nerd-font and restart service
|
||||
set -e
|
||||
|
||||
systemctl stop clide-web
|
||||
cp /mnt/media/Projects/clide/deploy/ttyd-nerd-font/build/ttyd /usr/local/bin/ttyd
|
||||
cp /mnt/media/Projects/clide/deploy/clide-web.service /etc/systemd/system/clide-web.service
|
||||
systemctl daemon-reload
|
||||
systemctl start clide-web
|
||||
|
||||
echo "Done. ttyd version: $(ttyd --version)"
|
||||
systemctl status clide-web --no-pager
|
||||
Reference in New Issue
Block a user