mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 19:06:34 +00:00
fix(cursor): correct Xcursor binary format (was missing version field)
Previous generator packed 5 fields in the image chunk header but Xcursor format needs 9 (header_size, type, nominal, version, w, h, xhot, yhot, delay). Missing version field → malformed → wlroots ignored it → fell back to default visible cursor. Now writes correct 68-byte Xcursor with all 9 header fields. Added more cursor names (x_cursor, pirate, sides). Also: terminal UI shows bash-style cwd$ prompt, separates command from output visually, auto-detects pwd after each command for prompt update.
This commit is contained in:
parent
ee980509c7
commit
70bdc3bb8b
2 changed files with 101 additions and 58 deletions
|
|
@ -106,47 +106,29 @@ plymouth-set-default-theme betterframe || true
|
||||||
CURSOR_DIR=/usr/share/icons/betterframe-empty/cursors
|
CURSOR_DIR=/usr/share/icons/betterframe-empty/cursors
|
||||||
install -d -m 755 "$CURSOR_DIR"
|
install -d -m 755 "$CURSOR_DIR"
|
||||||
install -m 644 /tmp/bf-files/cursor.theme /usr/share/icons/betterframe-empty/cursor.theme
|
install -m 644 /tmp/bf-files/cursor.theme /usr/share/icons/betterframe-empty/cursor.theme
|
||||||
# Generate a 1x1 transparent X cursor for every standard cursor name.
|
# Generate valid 1x1 transparent Xcursor files. Previous generator had a
|
||||||
# xcursorgen reads a config file and produces the binary Xcursor format.
|
# missing version field → malformed → wlroots fell back to default cursor.
|
||||||
# If xcursorgen is available (from x11-apps), use it; otherwise create
|
# Xcursor format: file header (16) + TOC entry (12) + image chunk (36+4=40) = 68 bytes.
|
||||||
# a minimal raw Xcursor binary (header + 1px transparent image).
|
python3 -c "
|
||||||
printf '1 0 0 /tmp/bf-transparent.png\n' > /tmp/bf-cursor.cfg
|
import struct, os
|
||||||
convert -size 1x1 xc:transparent /tmp/bf-transparent.png 2>/dev/null \
|
# File header: magic + header_size + version(1.0) + ntoc
|
||||||
|| python3 -c "
|
hdr = b'Xcur' + struct.pack('<III', 16, 0x00010000, 1)
|
||||||
import struct,sys
|
# TOC: type(image=0xfffd0002) + subtype(nominal=1) + position(28)
|
||||||
# Minimal 1x1 RGBA PNG
|
toc = struct.pack('<III', 0xfffd0002, 1, 28)
|
||||||
sys.stdout.buffer.write(b'\\x89PNG\\r\\n\\x1a\\n' + bytes([
|
# Image chunk header (36 bytes): header_size + type + nominal + version + w + h + xhot + yhot + delay
|
||||||
0,0,0,13,73,72,68,82,0,0,0,1,0,0,0,1,8,6,0,0,0,31,21,196,137,
|
img = struct.pack('<IIIIIIIII', 36, 0xfffd0002, 1, 1, 1, 1, 0, 0, 0)
|
||||||
0,0,0,10,73,68,65,84,120,156,98,0,0,0,2,0,1,226,33,188,51,
|
# 1 ARGB pixel, fully transparent
|
||||||
0,0,0,0,73,69,78,68,174,66,96,130]))
|
px = struct.pack('<I', 0)
|
||||||
" > /tmp/bf-transparent.png
|
data = hdr + toc + img + px
|
||||||
if command -v xcursorgen >/dev/null 2>&1; then
|
|
||||||
for name in default left_ptr arrow watch hand2 text xterm top_left_corner \
|
|
||||||
top_right_corner bottom_left_corner bottom_right_corner sb_h_double_arrow \
|
|
||||||
sb_v_double_arrow fleur crosshair question_arrow; do
|
|
||||||
xcursorgen /tmp/bf-cursor.cfg "$CURSOR_DIR/$name" 2>/dev/null || true
|
|
||||||
done
|
|
||||||
else
|
|
||||||
# Minimal Xcursor binary: magic + header + 1x1 ARGB image (4 bytes, all 0)
|
|
||||||
python3 -c "
|
|
||||||
import struct, sys, os
|
|
||||||
magic = b'Xcur'
|
|
||||||
header = struct.pack('<III', 16, 1, 1) # header_size, version, ntoc
|
|
||||||
toc = struct.pack('<III', 0xfffd0002, 36, 28) # type=image, subtype=1, position
|
|
||||||
chunk_header = struct.pack('<IIIII', 36, 0xfffd0002, 1, 1, 1) # size,type,subtype(nominal),width,height
|
|
||||||
xhot_yhot = struct.pack('<II', 0, 0)
|
|
||||||
delay = struct.pack('<I', 0)
|
|
||||||
pixel = struct.pack('<I', 0) # 1 ARGB pixel, fully transparent
|
|
||||||
data = magic + header + toc + chunk_header + xhot_yhot + delay + pixel
|
|
||||||
for name in ['default','left_ptr','arrow','watch','hand2','text','xterm',
|
for name in ['default','left_ptr','arrow','watch','hand2','text','xterm',
|
||||||
'top_left_corner','top_right_corner','bottom_left_corner',
|
'top_left_corner','top_right_corner','bottom_left_corner',
|
||||||
'bottom_right_corner','sb_h_double_arrow','sb_v_double_arrow',
|
'bottom_right_corner','sb_h_double_arrow','sb_v_double_arrow',
|
||||||
'fleur','crosshair','question_arrow']:
|
'fleur','crosshair','question_arrow','x_cursor','pirate',
|
||||||
path = os.path.join('$CURSOR_DIR', name)
|
'sb_left_arrow','sb_right_arrow','sb_up_arrow','sb_down_arrow',
|
||||||
with open(path, 'wb') as f: f.write(data)
|
'top_side','bottom_side','left_side','right_side']:
|
||||||
" 2>/dev/null || true
|
with open(os.path.join('$CURSOR_DIR', name), 'wb') as f:
|
||||||
fi
|
f.write(data)
|
||||||
rm -f /tmp/bf-cursor.cfg /tmp/bf-transparent.png
|
"
|
||||||
# Set as system default cursor theme
|
# Set as system default cursor theme
|
||||||
update-alternatives --install /usr/share/icons/default/index.theme x-cursor-theme \
|
update-alternatives --install /usr/share/icons/default/index.theme x-cursor-theme \
|
||||||
/usr/share/icons/betterframe-empty/cursor.theme 100 2>/dev/null || true
|
/usr/share/icons/betterframe-empty/cursor.theme 100 2>/dev/null || true
|
||||||
|
|
|
||||||
|
|
@ -1770,45 +1770,98 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
||||||
// WS auth: browser sends session cookie automatically on WS upgrade.
|
// WS auth: browser sends session cookie automatically on WS upgrade.
|
||||||
// Coordinator WS endpoint validates via resolveSession.
|
// Coordinator WS endpoint validates via resolveSession.
|
||||||
return htmlPage(`<html><head><title>Terminal: ${kiosk.name}</title>
|
return htmlPage(`<html><head><title>Terminal: ${kiosk.name}</title>
|
||||||
<style>body{margin:0;background:#000;color:#fff;font-family:monospace;font-size:14px;padding:1rem}
|
<style>
|
||||||
#term{white-space:pre-wrap;word-break:break-all;height:calc(100vh - 120px);overflow-y:auto;background:#111;padding:8px;border:1px solid #333}
|
body{margin:0;background:#1a1a1a;color:#e0e0e0;font-family:'Cascadia Code','Fira Code',monospace;font-size:13px;padding:1rem}
|
||||||
.controls{margin-bottom:1rem;display:flex;gap:8px;align-items:center}
|
#term{height:calc(100vh - 100px);overflow-y:auto;background:#0d0d0d;padding:12px;border:1px solid #333;border-radius:4px}
|
||||||
button{background:#333;color:#fff;border:1px solid #555;padding:4px 12px;cursor:pointer}
|
.line{margin:0;white-space:pre-wrap;word-break:break-all;line-height:1.5}
|
||||||
input{background:#222;color:#fff;border:1px solid #555;padding:4px 8px;font-family:monospace;width:200px}
|
.prompt{color:#5faf5f}
|
||||||
.status{color:#888;font-size:12px;margin-left:12px}
|
.cmd{color:#fff}
|
||||||
|
.output{color:#b0b0b0}
|
||||||
|
.controls{margin-bottom:0.75rem;display:flex;gap:8px;align-items:center}
|
||||||
|
button{background:#333;color:#fff;border:1px solid #555;padding:4px 12px;cursor:pointer;border-radius:3px}
|
||||||
|
button:hover{background:#444}
|
||||||
|
input{background:#222;color:#fff;border:1px solid #555;padding:4px 8px;font-family:inherit;border-radius:3px}
|
||||||
|
#code-input{width:180px}
|
||||||
|
.status{color:#666;font-size:11px;margin-left:12px}
|
||||||
|
.cmd-line{display:flex;align-items:center;margin-top:4px;background:#0d0d0d;border:1px solid #333;border-radius:4px;padding:4px 8px}
|
||||||
|
.cmd-line .prompt-label{color:#5faf5f;margin-right:8px;white-space:nowrap}
|
||||||
|
.cmd-line input{flex:1;background:transparent;border:none;color:#fff;outline:none;font-family:inherit;font-size:13px;padding:2px 0}
|
||||||
</style></head><body>
|
</style></head><body>
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<a href="/admin/kiosks/${id}" style="color:#0af">← ${kiosk.name}</a>
|
<a href="/admin/kiosks/${id}" style="color:#5fafff;text-decoration:none">← ${kiosk.name}</a>
|
||||||
<button id="btn-request">Request Terminal</button>
|
<button id="btn-request">Request Terminal</button>
|
||||||
<input id="code-input" placeholder="Enter code from kiosk screen" style="display:none" />
|
<input id="code-input" placeholder="Enter code from screen" style="display:none" />
|
||||||
<button id="btn-auth" style="display:none">Authenticate</button>
|
<button id="btn-auth" style="display:none">Auth</button>
|
||||||
<span class="status" id="status">Disconnected</span>
|
<span class="status" id="status">Disconnected</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="term"></div>
|
<div id="term"></div>
|
||||||
<input id="cmd-input" placeholder="Type here..." style="width:100%;background:#222;color:#fff;border:1px solid #333;padding:6px;font-family:monospace;margin-top:4px;display:none" />
|
<div class="cmd-line" id="cmd-row" style="display:none">
|
||||||
|
<span class="prompt-label" id="prompt-label">$</span>
|
||||||
|
<input id="cmd-input" placeholder="" autofocus />
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
var term=document.getElementById('term'),status=document.getElementById('status');
|
var term=document.getElementById('term'),status=document.getElementById('status');
|
||||||
var codeInput=document.getElementById('code-input'),authBtn=document.getElementById('btn-auth');
|
var codeInput=document.getElementById('code-input'),authBtn=document.getElementById('btn-auth');
|
||||||
var cmdInput=document.getElementById('cmd-input');
|
var cmdInput=document.getElementById('cmd-input'),cmdRow=document.getElementById('cmd-row');
|
||||||
var ws;
|
var promptLabel=document.getElementById('prompt-label');
|
||||||
|
var ws,cwd='~',outputBuf='';
|
||||||
|
|
||||||
|
function appendOutput(){
|
||||||
|
if(!outputBuf)return;
|
||||||
|
var div=document.createElement('div');
|
||||||
|
div.className='line output';
|
||||||
|
div.textContent=outputBuf;
|
||||||
|
term.appendChild(div);
|
||||||
|
outputBuf='';
|
||||||
|
term.scrollTop=term.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendCmd(cmd){
|
||||||
|
appendOutput();
|
||||||
|
var div=document.createElement('div');
|
||||||
|
div.className='line';
|
||||||
|
var p=document.createElement('span');
|
||||||
|
p.className='prompt';
|
||||||
|
p.textContent=cwd+'$ ';
|
||||||
|
var c=document.createElement('span');
|
||||||
|
c.className='cmd';
|
||||||
|
c.textContent=cmd;
|
||||||
|
div.appendChild(p);
|
||||||
|
div.appendChild(c);
|
||||||
|
term.appendChild(div);
|
||||||
|
term.scrollTop=term.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
function connect(){
|
function connect(){
|
||||||
var proto=location.protocol==='https:'?'wss:':'ws:';
|
var proto=location.protocol==='https:'?'wss:':'ws:';
|
||||||
ws=new WebSocket(proto+'//'+location.host+'/admin/ws/debug/${id}');
|
ws=new WebSocket(proto+'//'+location.host+'/admin/ws/debug/${id}');
|
||||||
ws.onopen=function(){status.textContent='Connected (not authed)';};
|
ws.onopen=function(){status.textContent='Connected';};
|
||||||
ws.onmessage=function(e){
|
ws.onmessage=function(e){
|
||||||
try{var m=JSON.parse(e.data);
|
try{var m=JSON.parse(e.data);
|
||||||
if(m.type==='terminal-challenge'){
|
if(m.type==='terminal-challenge'){
|
||||||
status.textContent='Code displayed on kiosk screen';
|
status.textContent='Enter code from kiosk screen';
|
||||||
codeInput.style.display='';authBtn.style.display='';
|
codeInput.style.display='';authBtn.style.display='';codeInput.focus();
|
||||||
}else if(m.type==='terminal-granted'){
|
}else if(m.type==='terminal-granted'){
|
||||||
status.textContent='Terminal active';
|
status.textContent='Terminal active';
|
||||||
codeInput.style.display='none';authBtn.style.display='none';
|
codeInput.style.display='none';authBtn.style.display='none';
|
||||||
cmdInput.style.display='';cmdInput.focus();
|
cmdRow.style.display='flex';cmdInput.focus();
|
||||||
|
// Get initial pwd
|
||||||
|
ws.send(JSON.stringify({type:'terminal-data',data:btoa('pwd\\n')}));
|
||||||
}else if(m.type==='terminal-denied'){
|
}else if(m.type==='terminal-denied'){
|
||||||
status.textContent='Denied: '+(m.reason||'unknown');
|
status.textContent='Denied: '+(m.reason||'unknown');
|
||||||
}else if(m.type==='terminal-data'){
|
}else if(m.type==='terminal-data'){
|
||||||
var bytes=atob(m.data);term.textContent+=bytes;term.scrollTop=term.scrollHeight;
|
var bytes=atob(m.data);
|
||||||
|
outputBuf+=bytes;
|
||||||
|
// Try to detect pwd from output (last non-empty line after command)
|
||||||
|
var lines=outputBuf.split('\\n');
|
||||||
|
for(var i=lines.length-1;i>=0;i--){
|
||||||
|
var l=lines[i].trim();
|
||||||
|
if(l&&l.startsWith('/')){cwd=l.replace(/^\\/home\\/bfkiosk/,'~');break;}
|
||||||
|
}
|
||||||
|
// Flush to display
|
||||||
|
appendOutput();
|
||||||
|
promptLabel.textContent=cwd+'$ ';
|
||||||
}
|
}
|
||||||
}catch{}
|
}catch{}
|
||||||
};
|
};
|
||||||
|
|
@ -1821,10 +1874,18 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
||||||
authBtn.onclick=function(){
|
authBtn.onclick=function(){
|
||||||
if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'terminal-auth',code:codeInput.value.toUpperCase()}));
|
if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'terminal-auth',code:codeInput.value.toUpperCase()}));
|
||||||
};
|
};
|
||||||
|
codeInput.onkeydown=function(e){
|
||||||
|
if(e.key==='Enter')authBtn.click();
|
||||||
|
};
|
||||||
cmdInput.onkeydown=function(e){
|
cmdInput.onkeydown=function(e){
|
||||||
if(e.key==='Enter'){
|
if(e.key==='Enter'&&cmdInput.value){
|
||||||
var text=cmdInput.value+'\\n';
|
var cmd=cmdInput.value;
|
||||||
if(ws&&ws.readyState===1)ws.send(JSON.stringify({type:'terminal-data',data:btoa(text)}));
|
appendCmd(cmd);
|
||||||
|
ws.send(JSON.stringify({type:'terminal-data',data:btoa(cmd+'\\n')}));
|
||||||
|
// After each command, get pwd for prompt update
|
||||||
|
setTimeout(function(){
|
||||||
|
ws.send(JSON.stringify({type:'terminal-data',data:btoa('pwd\\n')}));
|
||||||
|
},200);
|
||||||
cmdInput.value='';
|
cmdInput.value='';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue