feat(shell): add Ctrl+/- zoom and display all shortcuts in footer
All checks were successful
Beta Release / beta (push) Successful in 48s
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>
This commit is contained in:
@@ -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') },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -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 || 12
|
||||||
|
}, [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: '',
|
||||||
})
|
})
|
||||||
@@ -1059,6 +1107,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')}>
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user