Compare commits

...

4 Commits

Author SHA1 Message Date
Augustin
9a218b1904 fix(shell): set default terminal fontSize to 6px
All checks were successful
Beta Release / beta (push) Successful in 49s
All fallbacks were still using 12px. User confirmed 6px is the
correct baseline on their display.

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-24 22:41:47 +02:00
Augustin
399b845e14 fix(shell): default fontSize 10px and init new tabs immediately
All checks were successful
Beta Release / beta (push) Successful in 48s
- Base font size reduced from 12px to 10px
- New tabs now initialize directly when added (was waiting for
  tab switch because the MutationObserver only fired on visibility
  changes, not on tab additions)
- Zoom level applied to newly created terminals

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-24 22:33:49 +02:00
Augustin
436d5c6149 feat(shell): add Ctrl+/- zoom and display all shortcuts in footer
All checks were successful
Beta Release / beta (push) Successful in 48s
- Ctrl+/Ctrl-/Ctrl+0 to zoom in/out/reset terminal font size
- Zoom badge indicator in tab bar
- All shell shortcuts now shown in statusbar footer
- Added i18n labels for search, zoom, switch tab, next tab

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-24 22:28:15 +02:00
Augustin
5a9edc076e fix(deps): upgrade @xterm/xterm to 6.1.0-beta.203 for addon compatibility
All checks were successful
Beta Release / beta (push) Successful in 49s
The addon-web-links registerApcHandler API requires xterm >= 6.1.0.

💘 Generated with Crush

Assisted-by: GLM-5.1 via Crush <crush@charm.land>
2026-04-24 22:19:12 +02:00
7 changed files with 90 additions and 10 deletions

8
web/package-lock.json generated
View File

@@ -12,7 +12,7 @@
"@xterm/addon-unicode11": "^0.10.0-beta.203", "@xterm/addon-unicode11": "^0.10.0-beta.203",
"@xterm/addon-web-links": "^0.12.0", "@xterm/addon-web-links": "^0.12.0",
"@xterm/addon-webgl": "^0.20.0-beta.202", "@xterm/addon-webgl": "^0.20.0-beta.202",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^6.1.0-beta.203",
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"react": "^19.2.5", "react": "^19.2.5",
"react-dom": "^19.2.5" "react-dom": "^19.2.5"
@@ -453,9 +453,9 @@
} }
}, },
"node_modules/@xterm/xterm": { "node_modules/@xterm/xterm": {
"version": "6.0.0", "version": "6.1.0-beta.203",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-6.1.0-beta.203.tgz",
"integrity": "sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==", "integrity": "sha512-Ctqf05M6fPWZkfKxC4hy2+PP5P2BlVnJLbIsXZMpkCz/MjJvcf5OwwsGkq+nzhFDuojSX+rc2RxIetLONUBGqw==",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"addons/*" "addons/*"

View File

@@ -14,7 +14,7 @@
"@xterm/addon-unicode11": "^0.10.0-beta.203", "@xterm/addon-unicode11": "^0.10.0-beta.203",
"@xterm/addon-web-links": "^0.12.0", "@xterm/addon-web-links": "^0.12.0",
"@xterm/addon-webgl": "^0.20.0-beta.202", "@xterm/addon-webgl": "^0.20.0-beta.202",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^6.1.0-beta.203",
"lucide-react": "^1.8.0", "lucide-react": "^1.8.0",
"react": "^19.2.5", "react": "^19.2.5",
"react-dom": "^19.2.5" "react-dom": "^19.2.5"

View File

@@ -94,6 +94,10 @@ export default function App() {
shell: [ shell: [
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+C`, desc: t('statusbar.copy') }, { keys: `${layout.keys.ctrl}+${layout.keys.shift}+C`, desc: t('statusbar.copy') },
{ keys: `${layout.keys.ctrl}+${layout.keys.shift}+V`, desc: t('statusbar.paste') }, { keys: `${layout.keys.ctrl}+${layout.keys.shift}+V`, desc: t('statusbar.paste') },
{ keys: `${layout.keys.ctrl}+F`, desc: t('statusbar.search') },
{ keys: `${layout.keys.ctrl}+/Ctrl`, desc: t('statusbar.zoom') },
{ keys: `Alt+1-7`, desc: t('statusbar.switchTab') },
{ keys: `${layout.keys.shift}+Tab`, desc: t('statusbar.nextTab') },
{ keys: layout.keys.enter, desc: t('statusbar.runCommand') }, { keys: layout.keys.enter, desc: t('statusbar.runCommand') },
{ keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') }, { keys: `${layout.keys.up}/${layout.keys.down}`, desc: t('statusbar.commandHistory') },
], ],

View File

@@ -204,7 +204,7 @@ function createTerminal(container, settings = {}) {
const term = new XTerm({ const term = new XTerm({
cursorBlink: true, cursorBlink: true,
allowProposedApi: true, allowProposedApi: true,
fontSize: settings.fontSize || 12, fontSize: settings.fontSize || 6,
fontFamily: settings.fontFamily || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace", fontFamily: settings.fontFamily || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
theme, theme,
allowTransparency: false, allowTransparency: false,
@@ -255,6 +255,27 @@ function createTerminal(container, settings = {}) {
return false return false
} }
if (ctrl && (e.key === '=' || e.key === '+')) {
e.preventDefault()
e.stopPropagation()
window.dispatchEvent(new CustomEvent('shell-zoom', { detail: 1 }))
return false
}
if (ctrl && e.key === '-') {
e.preventDefault()
e.stopPropagation()
window.dispatchEvent(new CustomEvent('shell-zoom', { detail: -1 }))
return false
}
if (ctrl && e.key === '0') {
e.preventDefault()
e.stopPropagation()
window.dispatchEvent(new CustomEvent('shell-zoom', { detail: 0 }))
return false
}
return true return true
}) })
@@ -329,7 +350,7 @@ export default function Shell({ api }) {
const { t } = useI18n() const { t } = useI18n()
const tabsRef = useRef({}) const tabsRef = useRef({})
const nextIdRef = useRef(1) const nextIdRef = useRef(1)
const settingsRef = useRef({ fontSize: 12, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'system' }) const settingsRef = useRef({ fontSize: 6, fontFamily: "'JetBrains Mono', 'Fira Code', monospace", theme: 'system' })
const pendingCommandsRef = useRef({}) const pendingCommandsRef = useRef({})
const [tabs, setTabs] = useState(() => { const [tabs, setTabs] = useState(() => {
@@ -378,7 +399,7 @@ export default function Shell({ api }) {
const [editingTab, setEditingTab] = useState(null) const [editingTab, setEditingTab] = useState(null)
const [editName, setEditName] = useState('') const [editName, setEditName] = useState('')
const [terminalSettings, setTerminalSettings] = useState({ const [terminalSettings, setTerminalSettings] = useState({
fontSize: 12, fontSize: 6,
fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace", fontFamily: "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
theme: 'system', theme: 'system',
}) })
@@ -387,9 +408,36 @@ export default function Shell({ api }) {
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const searchInputRef = useRef(null) const searchInputRef = useRef(null)
const searchDecorationsRef = useRef(null) const searchDecorationsRef = useRef(null)
const [zoomLevel, setZoomLevel] = useState(0)
const baseFontSizeRef = useRef(12)
useEffect(() => { settingsRef.current = terminalSettings }, [terminalSettings]) useEffect(() => { settingsRef.current = terminalSettings }, [terminalSettings])
useEffect(() => {
baseFontSizeRef.current = terminalSettings.fontSize || 6
}, [terminalSettings.fontSize])
useEffect(() => {
const handler = (e) => {
const direction = e.detail
setZoomLevel(prev => {
let next
if (direction === 0) next = 0
else next = Math.max(-8, Math.min(10, prev + direction))
const newSize = baseFontSizeRef.current + next * 2
for (const entry of Object.values(tabsRef.current)) {
if (entry.term && !entry.term._disposed) {
entry.term.options.fontSize = newSize
try { entry.fitAddon.fit() } catch {}
}
}
return next
})
}
window.addEventListener('shell-zoom', handler)
return () => window.removeEventListener('shell-zoom', handler)
}, [])
const [sshForm, setSshForm] = useState({ const [sshForm, setSshForm] = useState({
name: '', host: '', port: 22, user: '', key_path: '', name: '', host: '', port: 22, user: '', key_path: '',
}) })
@@ -448,7 +496,7 @@ export default function Shell({ api }) {
api.getConfig().then(d => { api.getConfig().then(d => {
if (d.terminal) { if (d.terminal) {
setTerminalSettings({ setTerminalSettings({
fontSize: d.terminal.font_size || 12, fontSize: d.terminal.font_size || 6,
fontFamily: d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace", fontFamily: d.terminal.font_family || "'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'SF Mono', 'Menlo', monospace",
theme: d.terminal.theme || 'system', theme: d.terminal.theme || 'system',
}) })
@@ -463,8 +511,9 @@ export default function Shell({ api }) {
if (!container) return if (!container) return
const s = settingsRef.current const s = settingsRef.current
const effectiveFontSize = s.fontSize + zoomLevel * 2
const { term, fitAddon, searchAddon } = createTerminal(container, { const { term, fitAddon, searchAddon } = createTerminal(container, {
fontSize: s.fontSize, fontSize: effectiveFontSize,
fontFamily: s.fontFamily, fontFamily: s.fontFamily,
theme: s.theme, theme: s.theme,
}) })
@@ -658,6 +707,12 @@ export default function Shell({ api }) {
}) })
} }
for (const tab of tabs) {
if (!tabsRef.current[tab.id]) {
tryInitTab(tab, 0)
}
}
const wrapper = document.querySelector('.shell-layout')?.parentElement const wrapper = document.querySelector('.shell-layout')?.parentElement
let observer let observer
if (wrapper) { if (wrapper) {
@@ -1059,6 +1114,11 @@ export default function Shell({ api }) {
</div> </div>
<div className="shell-tab-actions"> <div className="shell-tab-actions">
{zoomLevel !== 0 && (
<span className="shell-zoom-badge">
{zoomLevel > 0 ? '+' : ''}{zoomLevel > 0 ? zoomLevel * 2 : zoomLevel * 2}px
</span>
)}
{tabs.length < MAX_TABS && ( {tabs.length < MAX_TABS && (
<div className="shell-new-tab-wrapper"> <div className="shell-new-tab-wrapper">
<button className="shell-new-tab-btn" onClick={() => setShowMenu(!showMenu)} title={t('shell.newTab')}> <button className="shell-new-tab-btn" onClick={() => setShowMenu(!showMenu)} title={t('shell.newTab')}>

View File

@@ -18,6 +18,10 @@ const en = {
newLine: 'New line', newLine: 'New line',
copy: 'Copy', copy: 'Copy',
paste: 'Paste', paste: 'Paste',
search: 'Search',
zoom: 'Zoom +/',
switchTab: 'Switch tab',
nextTab: 'Next tab',
runCommand: 'Run command', runCommand: 'Run command',
commandHistory: 'Command history', commandHistory: 'Command history',
}, },

View File

@@ -18,6 +18,10 @@ const fr = {
newLine: 'Nouvelle ligne', newLine: 'Nouvelle ligne',
copy: 'Copier', copy: 'Copier',
paste: 'Coller', paste: 'Coller',
search: 'Rechercher',
zoom: 'Zoom +/\u2212',
switchTab: 'Changer d\u2019onglet',
nextTab: 'Onglet suivant',
runCommand: 'Ex\u00e9cuter', runCommand: 'Ex\u00e9cuter',
commandHistory: 'Historique', commandHistory: 'Historique',
}, },

View File

@@ -329,6 +329,14 @@ input::placeholder { color: var(--text-disabled); }
.shell-tab-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; } .shell-tab-actions { display: flex; align-items: center; gap: 4px; flex-shrink: 0; }
.shell-zoom-badge {
font-size: 10px; font-family: var(--font-mono); font-weight: 600;
color: var(--accent); background: var(--accent-bg);
padding: 2px 6px; border-radius: 3px;
border: 1px solid var(--accent-dim);
white-space: nowrap;
}
.shell-new-tab-wrapper { position: relative; } .shell-new-tab-wrapper { position: relative; }
.shell-new-tab-btn { .shell-new-tab-btn {
display: flex; align-items: center; gap: 2px; display: flex; align-items: center; gap: 2px;