mirror of
https://github.com/quine-global/hyper.git
synced 2026-11-03 21:49:40 -09:00
Fix linux issues (#375)
* Support scroll on linux * Close tab on middle-click of the X button too The X icon only had onClick, which browsers never fire for the middle button. Wire it to the same middle-click handler already used on the tab body so middle-clicking the X closes the tab like clicking anywhere else on it does. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Spawn shell via flatpak-spawn --host on Flatpak builds The Flatpak build sandboxes the shell inside the app's mount namespace, so PATH ends up as whatever the runtime provides (/app/bin:/usr/bin) with no access to /usr/local or /home/linuxbrew, unlike an unsandboxed terminal like Konsole. Route the pty through flatpak-spawn --host so the shell actually runs on the host and its --login profile computes a normal host PATH, and grant the org.freedesktop.Flatpak talk-name the portal needs for this. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Document Flatpak shell-spawning fix and bare-shell-name limitation Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * Fix prettier lint in session.ts and tabs.tsx Both fit within the 120-column print width, so prettier wants them on one line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
c066cdfabc
commit
e2de7e18d1
6 changed files with 96 additions and 5 deletions
|
|
@ -10,6 +10,7 @@ import * as config from './config';
|
|||
import {cliScriptPath} from './config/paths';
|
||||
import {productName, version} from './package.json';
|
||||
import {getDecoratedEnv} from './plugins';
|
||||
import {isFlatpak} from './utils/flatpak';
|
||||
import {getFallBackShellConfig} from './utils/shell-fallback';
|
||||
|
||||
const createNodePtyError = () =>
|
||||
|
|
@ -154,11 +155,28 @@ export default class Session extends EventEmitter {
|
|||
delete baseEnv.GOOGLE_API_KEY;
|
||||
}
|
||||
|
||||
const ptyEnv = getDecoratedEnv(baseEnv);
|
||||
|
||||
// When running as a Flatpak, this process (and anything node-pty spawns directly)
|
||||
// is confined to the sandbox's mount namespace, where PATH is whatever the runtime
|
||||
// sets (e.g. /app/bin:/usr/bin) and host-only locations like /usr/local or
|
||||
// /home/linuxbrew aren't even mounted. Route the shell through `flatpak-spawn --host`
|
||||
// so it actually runs on the host, like a normal terminal, and drop our sandboxed
|
||||
// PATH so the host login shell computes its own from the host's profile scripts
|
||||
// instead of inheriting the sandbox's.
|
||||
let spawnCommand = shell;
|
||||
let spawnArgs = shellArgs;
|
||||
if (isFlatpak()) {
|
||||
delete ptyEnv['PATH'];
|
||||
spawnCommand = 'flatpak-spawn';
|
||||
spawnArgs = ['--host', '--watch-bus', ...(cwd ? [`--directory=${cwd}`] : []), shell, ...shellArgs];
|
||||
}
|
||||
|
||||
const options: IWindowsPtyForkOptions = {
|
||||
cols,
|
||||
rows,
|
||||
cwd,
|
||||
env: getDecoratedEnv(baseEnv)
|
||||
env: ptyEnv
|
||||
};
|
||||
|
||||
// if config do not set the useConpty, it will be judged by the node-pty
|
||||
|
|
@ -166,9 +184,9 @@ export default class Session extends EventEmitter {
|
|||
options.useConpty = useConpty;
|
||||
}
|
||||
|
||||
console.log('[session] spawning pty, shell:', shell, 'args:', shellArgs, 'useConpty:', options.useConpty);
|
||||
console.log('[session] spawning pty, command:', spawnCommand, 'args:', spawnArgs, 'useConpty:', options.useConpty);
|
||||
try {
|
||||
this.pty = spawn(shell, shellArgs, options);
|
||||
this.pty = spawn(spawnCommand, spawnArgs, options);
|
||||
console.log('[session] pty spawned, pid:', this.pty.pid);
|
||||
} catch (_err) {
|
||||
const err = _err as {message: string; stack?: string};
|
||||
|
|
|
|||
5
app/utils/flatpak.ts
Normal file
5
app/utils/flatpak.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import {existsSync} from 'fs';
|
||||
|
||||
// Every process running inside a Flatpak sandbox has FLATPAK_ID set and
|
||||
// a /.flatpak-info file bind-mounted in, regardless of runtime or app.
|
||||
export const isFlatpak = (): boolean => Boolean(process.env.FLATPAK_ID) || existsSync('/.flatpak-info');
|
||||
58
docs/flatpak-shell.md
Normal file
58
docs/flatpak-shell.md
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# Shell spawning under Flatpak
|
||||
|
||||
## Background
|
||||
|
||||
Hyper's Flatpak build (`electron-builder.json` → `flatpak`) sandboxes the app.
|
||||
Historically this meant the pty shell was spawned *inside* the sandbox's mount
|
||||
namespace, where `PATH` is whatever the runtime provides
|
||||
(`/app/bin:/usr/bin`) and host-only locations like `/usr/local` or
|
||||
`/home/linuxbrew` aren't mounted at all — unlike an unsandboxed terminal such
|
||||
as Konsole, which inherits the full host `PATH` from the user's shell
|
||||
profile.
|
||||
|
||||
## Fix
|
||||
|
||||
`app/session.ts` now detects the sandbox via `app/utils/flatpak.ts`
|
||||
(`isFlatpak()`, checking `FLATPAK_ID` / `/.flatpak-info`) and, when sandboxed,
|
||||
spawns the shell through `flatpak-spawn --host` instead of directly:
|
||||
|
||||
```
|
||||
flatpak-spawn --host --watch-bus --directory=<cwd> <shell> <shellArgs>
|
||||
```
|
||||
|
||||
`--host` runs `<shell>` on the host via the `org.freedesktop.Flatpak` portal,
|
||||
so it's the real host binary (e.g. the host's `/bin/bash`), not anything
|
||||
from `/app` or the runtime's `/usr`. The sandboxed `PATH` is deleted from the
|
||||
env passed to `flatpak-spawn` so the host's own `--login` shell profile
|
||||
computes `PATH` from scratch, matching what Konsole would produce. All other
|
||||
env vars (`DISPLAY`, `DBUS_SESSION_BUS_ADDRESS`, `XDG_RUNTIME_DIR`,
|
||||
`SSH_AUTH_SOCK`, `TERM`, etc.) are still forwarded as-is.
|
||||
|
||||
`electron-builder.json`'s Flatpak `finishArgs` gained
|
||||
`--talk-name=org.freedesktop.Flatpak`, which `flatpak-spawn --host` requires.
|
||||
|
||||
Non-Flatpak builds (deb/rpm/snap/pacman/AppImage/Win/Mac) are unaffected —
|
||||
they still spawn the shell directly.
|
||||
|
||||
## Known limitation: bare shell names
|
||||
|
||||
`<shell>` is passed to `flatpak-spawn --host` as a literal argument and is
|
||||
**not** resolved against any `PATH` — it only works because the shell value
|
||||
is normally an absolute path (`default-shell` resolves `process.env.SHELL`,
|
||||
falling back to `/bin/bash`).
|
||||
|
||||
If a user's Hyper profile config sets a custom `shell` as a **bare command
|
||||
name** instead of an absolute path (e.g. `"shell": "fish"` rather than
|
||||
`"shell": "/usr/bin/fish"`), spawning will fail under the Flatpak build:
|
||||
there's no `PATH` on the host-exec side to search, since `PATH` is
|
||||
intentionally stripped before the sandboxed `PATH` value can leak to the
|
||||
host's `--login` shell (see "Fix" above).
|
||||
|
||||
This is a pre-existing footgun made slightly worse by this change — not
|
||||
currently handled specially. If it needs fixing, the options are:
|
||||
|
||||
- Resolve bare shell names to absolute paths before constructing
|
||||
`spawnArgs` in `app/session.ts` (e.g. via a `flatpak-spawn --host which
|
||||
<shell>` lookup, or requiring absolute paths in profile config).
|
||||
- Document the requirement in `README.md`/config docs so profile authors
|
||||
know to use absolute paths for `shell` on Linux.
|
||||
|
|
@ -129,6 +129,7 @@
|
|||
"--filesystem=home",
|
||||
"--share=network",
|
||||
"--talk-name=org.freedesktop.Notifications",
|
||||
"--talk-name=org.freedesktop.Flatpak",
|
||||
"--socket=ssh-auth"
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ function Tab(props: TabProps) {
|
|||
{props.text}
|
||||
</span>
|
||||
</span>
|
||||
<i className="tab_icon" onClick={props.onClose}>
|
||||
<i className="tab_icon" onClick={props.onClose} onMouseUp={handleMouseUp}>
|
||||
<svg className="tab_shape">
|
||||
<use xlinkHref="./renderer/assets/icons.svg#close-tab" />
|
||||
</svg>
|
||||
|
|
|
|||
|
|
@ -33,13 +33,22 @@ const Tabs = forwardRef<HTMLElement, TabsProps>((props, ref) => {
|
|||
|
||||
const hide = !isMac && tabs.length === 1;
|
||||
|
||||
const handleWheel = (event: React.WheelEvent<HTMLUListElement>) => {
|
||||
// A plain vertical mouse wheel only ever reports deltaY. macOS trackpads
|
||||
// generate real deltaX for horizontal swipes, but a wheel mouse (common
|
||||
// on Linux/Windows) doesn't, so without this the tab list never scrolls.
|
||||
if (event.deltaX === 0 && event.deltaY !== 0) {
|
||||
event.currentTarget.scrollLeft += event.deltaY;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<nav className={`tabs_nav ${hide ? 'tabs_hiddenNav' : ''}`} ref={ref}>
|
||||
{props.customChildrenBefore}
|
||||
{tabs.length === 1 && isMac ? <div className="tabs_title">{tabs[0].title}</div> : null}
|
||||
{tabs.length > 1 ? (
|
||||
<>
|
||||
<ul key="list" className={`tabs_list ${fullScreen && isMac ? 'tabs_fullScreen' : ''}`}>
|
||||
<ul key="list" className={`tabs_list ${fullScreen && isMac ? 'tabs_fullScreen' : ''}`} onWheel={handleWheel}>
|
||||
{tabs.map((tab, i) => {
|
||||
const {uid, title, isActive, hasActivity} = tab;
|
||||
const tabProps = getTabProps(tab, props, {
|
||||
|
|
|
|||
Loading…
Reference in a new issue