T05: shell integration for zsh, bash, fish, PowerShell; installed at start
This commit is contained in:
12 files changed
+375
-4
No files matched your search
@@ -35,6 +35,7 @@ jobs:
|
|||||||
# The Linux job container runs as root, which ignores the file permissions the tests check.
|
# The Linux job container runs as root, which ignores the file permissions the tests check.
|
||||||
- if: runner.os == 'Linux'
|
- if: runner.os == 'Linux'
|
||||||
run: |
|
run: |
|
||||||
|
apt-get update && apt-get install -y zsh fish
|
||||||
useradd --create-home ci
|
useradd --create-home ci
|
||||||
chmod o+x "$HOME"
|
chmod o+x "$HOME"
|
||||||
chown -R ci "$GITHUB_WORKSPACE" "$HOME/.cargo" "$HOME/.rustup"
|
chown -R ci "$GITHUB_WORKSPACE" "$HOME/.cargo" "$HOME/.rustup"
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
|||||||
- `src/main.rs`: start, window. `src/lib.rs`: the modules, public so tests reach them.
|
- `src/main.rs`: start, window. `src/lib.rs`: the modules, public so tests reach them.
|
||||||
- `src/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`.
|
- `src/settings`: the model, its ranges, and `SettingsStore`, the global that loads, checks and saves `settings.json`.
|
||||||
- `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance.
|
- `src/themes`: built-in terminal themes, the chrome palette each maps to, and `apply`, which dresses the app for the system's appearance.
|
||||||
- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 7 before the parser. `unix` starts the program itself, so a failed `chdir` fails the start; Windows uses alacritty's ConPTY.
|
- `src/term`: the terminal engine. `pty` starts a program through alacritty's event loop; `tap` wraps the pty so its reader scans OSC 7 before the parser. `unix` starts the program itself, so a failed `chdir` fails the start; Windows uses alacritty's ConPTY. `shell` writes the integration scripts at start and builds each shell's launch.
|
||||||
|
- `assets/shell-integration`: ELYTerminal's own scripts (not Ghostty's, which are GPL): zsh through `ZDOTDIR`, bash through `--rcfile`, fish through `XDG_DATA_DIRS`, PowerShell through `-NoExit -Command`. Each prompt sends OSC 7 and, except in fish, whose `fish_title` stays, a title.
|
||||||
- `assets/themes`: Ghostty theme files copied verbatim from iTerm2-Color-Schemes (MIT, its `LICENSE` beside them).
|
- `assets/themes`: Ghostty theme files copied verbatim from iTerm2-Color-Schemes (MIT, its `LICENSE` beside them).
|
||||||
|
|
||||||
## Rules
|
## Rules
|
||||||
@@ -38,4 +39,5 @@ ELYTerminal. A terminal on GPUI and Ely components. `PRD.md` says what it does;
|
|||||||
- 2026-10-14: Git through the `git` command line, not libgit2.
|
- 2026-10-14: Git through the `git` command line, not libgit2.
|
||||||
- 2026-10-14: license MIT, as Ely.
|
- 2026-10-14: license MIT, as Ely.
|
||||||
- 2026-10-14: on Unix the pty is ELYTerminal's own: alacritty 0.26 ignores `chdir`'s result, so a session could start in the wrong folder. Its traits feed alacritty's event loop, keyed 0 and 1 as alacritty's private constants are; alacritty is pinned to =0.26.0 for that.
|
- 2026-10-14: on Unix the pty is ELYTerminal's own: alacritty 0.26 ignores `chdir`'s result, so a session could start in the wrong folder. Its traits feed alacritty's event loop, keyed 0 and 1 as alacritty's private constants are; alacritty is pinned to =0.26.0 for that.
|
||||||
|
- 2026-10-14: bash integration loads through `--rcfile`, not Ghostty's `--posix` and `ENV`: macOS's bash 3.2 ignores `ENV` there. bash then runs as a non-login shell; on macOS the script reads the login files itself, so `logout` and `~/.bash_logout` do not apply.
|
||||||
- 2026-10-14: settings refuse unknown fields; a bad settings file stops the start and is never overwritten.
|
- 2026-10-14: settings refuse unknown fields; a bad settings file stops the start and is never overwritten.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# ELYTerminal: load the usual startup files, report folder and title.
|
||||||
|
if [ -n "${ELY_BASH_LOGIN-}" ]; then
|
||||||
|
builtin unset ELY_BASH_LOGIN
|
||||||
|
[ -r /etc/profile ] && builtin source /etc/profile
|
||||||
|
for _ely_file in ~/.bash_profile ~/.bash_login ~/.profile; do
|
||||||
|
if [ -r "$_ely_file" ]; then
|
||||||
|
builtin source "$_ely_file"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
builtin unset _ely_file
|
||||||
|
else
|
||||||
|
[ -r ~/.bashrc ] && builtin source ~/.bashrc
|
||||||
|
fi
|
||||||
|
_ely_prompt() {
|
||||||
|
builtin printf '\e]7;file://%s\a\e]2;%s\a' "${PWD//\%/%25}" "${PWD##*/}"
|
||||||
|
}
|
||||||
|
PROMPT_COMMAND="_ely_prompt${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# ELYTerminal: restore XDG_DATA_DIRS, report the folder; fish_title keeps the title.
|
||||||
|
if test -n "$ELY_FISH_XDG_DATA_DIRS"
|
||||||
|
set -gx XDG_DATA_DIRS $ELY_FISH_XDG_DATA_DIRS
|
||||||
|
else
|
||||||
|
set -e XDG_DATA_DIRS
|
||||||
|
end
|
||||||
|
set -e ELY_FISH_XDG_DATA_DIRS
|
||||||
|
if status is-interactive
|
||||||
|
function _ely_prompt --on-event fish_prompt
|
||||||
|
printf '\e]7;file://%s\a' (string replace -a % %25 -- $PWD)
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
# ELYTerminal: wrap the prompt to report folder and title.
|
||||||
|
$global:ElyPrompt = $function:prompt
|
||||||
|
function global:prompt {
|
||||||
|
$folder = $executionContext.SessionState.Path.CurrentLocation
|
||||||
|
if ($folder.Provider.Name -eq 'FileSystem') {
|
||||||
|
$path = $folder.ProviderPath.Replace('\', '/').Replace('%', '%25')
|
||||||
|
if (-not $path.StartsWith('/')) { $path = "/$path" }
|
||||||
|
$leaf = Split-Path -Leaf $folder.ProviderPath
|
||||||
|
$esc, $bel = [char]27, [char]7
|
||||||
|
[Console]::Write("$esc]7;file://$path$bel$esc]2;$leaf$bel")
|
||||||
|
}
|
||||||
|
& $global:ElyPrompt
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# ELYTerminal: restore ZDOTDIR, load the user's .zshenv, report folder and title.
|
||||||
|
if [[ -n "${ELY_ZSH_ZDOTDIR+set}" ]]; then
|
||||||
|
ZDOTDIR=$ELY_ZSH_ZDOTDIR
|
||||||
|
builtin unset ELY_ZSH_ZDOTDIR
|
||||||
|
else
|
||||||
|
builtin unset ZDOTDIR
|
||||||
|
fi
|
||||||
|
[[ -r "${ZDOTDIR:-$HOME}/.zshenv" ]] && builtin source "${ZDOTDIR:-$HOME}/.zshenv"
|
||||||
|
if [[ -o interactive ]]; then
|
||||||
|
_ely_precmd() {
|
||||||
|
builtin printf '\e]7;file://%s\a\e]2;%s\a' "${PWD//\%/%25}" "${${PWD:t}:-/}"
|
||||||
|
}
|
||||||
|
_ely_preexec() {
|
||||||
|
builtin printf '\e]2;%s\a' "${1//[[:cntrl:]]/}"
|
||||||
|
}
|
||||||
|
builtin autoload -Uz add-zsh-hook
|
||||||
|
add-zsh-hook precmd _ely_precmd
|
||||||
|
add-zsh-hook preexec _ely_preexec
|
||||||
|
fi
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
use ely_gpui_component::{Assets, primitives::FocusScope, theme::ActiveTheme};
|
use ely_gpui_component::{Assets, primitives::FocusScope, theme::ActiveTheme};
|
||||||
use elyterminal::{settings::SettingsStore, themes};
|
use elyterminal::{settings::SettingsStore, term::shell::ShellIntegration, themes};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
App, AppContext, Bounds, Context, FocusHandle, IntoElement, Render, Styled, TitlebarOptions,
|
App, AppContext, Bounds, Context, FocusHandle, IntoElement, Render, Styled, TitlebarOptions,
|
||||||
Window, WindowBounds, WindowOptions, px, size,
|
Window, WindowBounds, WindowOptions, px, size,
|
||||||
@@ -34,7 +34,7 @@ fn main() {
|
|||||||
.init();
|
.init();
|
||||||
application().with_assets(Assets).run(|cx: &mut App| {
|
application().with_assets(Assets).run(|cx: &mut App| {
|
||||||
ely_gpui_component::init(cx).expect("Ely failed to start");
|
ely_gpui_component::init(cx).expect("Ely failed to start");
|
||||||
if let Err(error) = SettingsStore::init(cx) {
|
if let Err(error) = SettingsStore::init(cx).and_then(|()| ShellIntegration::init(cx)) {
|
||||||
log::error!("{error:#}");
|
log::error!("{error:#}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod pty;
|
mod pty;
|
||||||
|
pub mod shell;
|
||||||
mod tap;
|
mod tap;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
mod unix;
|
mod unix;
|
||||||
|
|||||||
@@ -0,0 +1,138 @@
|
|||||||
|
use std::{
|
||||||
|
env, fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Context as _;
|
||||||
|
use gpui::{App, Global};
|
||||||
|
|
||||||
|
use super::pty::Launch;
|
||||||
|
|
||||||
|
/// Each script, by its path under the integration folder.
|
||||||
|
const SCRIPTS: [(&str, &str); 4] = [
|
||||||
|
(
|
||||||
|
"zsh/.zshenv",
|
||||||
|
include_str!("../../assets/shell-integration/zsh/.zshenv"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"bash/elyterminal.bash",
|
||||||
|
include_str!("../../assets/shell-integration/bash/elyterminal.bash"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"fish/vendor_conf.d/elyterminal.fish",
|
||||||
|
include_str!("../../assets/shell-integration/fish/vendor_conf.d/elyterminal.fish"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"pwsh/elyterminal.ps1",
|
||||||
|
include_str!("../../assets/shell-integration/pwsh/elyterminal.ps1"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The folder the scripts live in, written at start.
|
||||||
|
pub struct ShellIntegration(pub PathBuf);
|
||||||
|
|
||||||
|
impl Global for ShellIntegration {}
|
||||||
|
|
||||||
|
impl ShellIntegration {
|
||||||
|
/// Writes the scripts into the platform's data folder and keeps its path.
|
||||||
|
pub fn init(cx: &mut App) -> anyhow::Result<()> {
|
||||||
|
let dir = dirs::data_local_dir()
|
||||||
|
.context("shell integration: the platform names no data folder")?
|
||||||
|
.join("ELYTerminal")
|
||||||
|
.join("shell-integration");
|
||||||
|
install(&dir)?;
|
||||||
|
cx.set_global(Self(dir));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the scripts into `dir`, replacing any that differ.
|
||||||
|
pub fn install(dir: &Path) -> anyhow::Result<()> {
|
||||||
|
for (name, text) in SCRIPTS {
|
||||||
|
let path = dir.join(name);
|
||||||
|
if fs::read_to_string(&path).is_ok_and(|kept| kept == text) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let folder = path
|
||||||
|
.parent()
|
||||||
|
.context("shell integration: a script has no folder")?;
|
||||||
|
fs::create_dir_all(folder)
|
||||||
|
.with_context(|| format!("shell integration: cannot create {}", folder.display()))?;
|
||||||
|
fs::write(&path, text)
|
||||||
|
.with_context(|| format!("shell integration: cannot write {}", path.display()))?;
|
||||||
|
log::info!("shell integration: wrote {}", path.display());
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How to start `shell`, or the login shell when empty, in `cwd` so it reports its folder.
|
||||||
|
pub fn launch(shell: &str, cwd: PathBuf, dir: &Path) -> anyhow::Result<Launch> {
|
||||||
|
let path = if shell.is_empty() {
|
||||||
|
default_shell()?
|
||||||
|
} else {
|
||||||
|
shell.to_string()
|
||||||
|
};
|
||||||
|
let name = Path::new(&path)
|
||||||
|
.file_stem()
|
||||||
|
.map(|stem| stem.to_string_lossy().to_lowercase())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mut launch = Launch {
|
||||||
|
program: (!shell.is_empty()).then(|| (path.clone(), Vec::new())),
|
||||||
|
cwd,
|
||||||
|
env: Vec::new(),
|
||||||
|
};
|
||||||
|
let script = |name: &str| dir.join(name).display().to_string();
|
||||||
|
match name.as_str() {
|
||||||
|
"zsh" => {
|
||||||
|
if let Ok(zdotdir) = env::var("ZDOTDIR") {
|
||||||
|
launch.env.push(("ELY_ZSH_ZDOTDIR".into(), zdotdir));
|
||||||
|
}
|
||||||
|
launch.env.push(("ZDOTDIR".into(), script("zsh")));
|
||||||
|
}
|
||||||
|
"bash" => {
|
||||||
|
let rcfile = script("bash/elyterminal.bash");
|
||||||
|
launch.program = Some((path.clone(), vec!["--rcfile".into(), rcfile]));
|
||||||
|
if cfg!(target_os = "macos") {
|
||||||
|
launch.env.push(("ELY_BASH_LOGIN".into(), "1".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"fish" => {
|
||||||
|
let previous = env::var("XDG_DATA_DIRS").unwrap_or_default();
|
||||||
|
let data = if previous.is_empty() {
|
||||||
|
format!("{}:/usr/local/share:/usr/share", script("fish"))
|
||||||
|
} else {
|
||||||
|
format!("{}:{previous}", script("fish"))
|
||||||
|
};
|
||||||
|
launch.env.push(("XDG_DATA_DIRS".into(), data));
|
||||||
|
launch.env.push(("ELY_FISH_XDG_DATA_DIRS".into(), previous));
|
||||||
|
}
|
||||||
|
"pwsh" | "powershell" => {
|
||||||
|
let file = script("pwsh/elyterminal.ps1").replace('\'', "''");
|
||||||
|
launch.program = Some((
|
||||||
|
path.clone(),
|
||||||
|
vec![
|
||||||
|
"-NoLogo".into(),
|
||||||
|
"-NoExit".into(),
|
||||||
|
"-Command".into(),
|
||||||
|
format!(". '{file}'"),
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => log::info!("shell integration: none for {path}; its folder goes unreported"),
|
||||||
|
}
|
||||||
|
log::info!("shell integration: {path} as {name}");
|
||||||
|
Ok(launch)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn default_shell() -> anyhow::Result<String> {
|
||||||
|
Ok(super::unix::login_shell_path()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn default_shell() -> anyhow::Result<String> {
|
||||||
|
Ok("powershell.exe".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, unix))]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use alacritty_terminal::event::Event;
|
||||||
|
use futures::channel::mpsc::TryRecvError;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::term::{PtyEvent, spawn};
|
||||||
|
|
||||||
|
/// A fresh folder under the temp dir, by its real path.
|
||||||
|
fn folder(name: &str) -> PathBuf {
|
||||||
|
let folder = env::temp_dir().join(format!("elyterminal-{name}-{}", std::process::id()));
|
||||||
|
if folder.exists() {
|
||||||
|
fs::remove_dir_all(&folder).expect("a stale test folder goes");
|
||||||
|
}
|
||||||
|
fs::create_dir_all(&folder).expect("a test folder");
|
||||||
|
folder.canonicalize().expect("a real path")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn installed(shell: &str) -> String {
|
||||||
|
let found = std::process::Command::new("/bin/sh")
|
||||||
|
.args(["-c", &format!("command -v {shell}")])
|
||||||
|
.output()
|
||||||
|
.expect("sh runs");
|
||||||
|
let path = String::from_utf8(found.stdout).expect("utf-8");
|
||||||
|
assert!(found.status.success(), "{shell} is not installed");
|
||||||
|
path.trim().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts `shell` with integration in a fresh folder, cds once, and returns folders and titles it reported.
|
||||||
|
fn reports(shell: &str) -> (PathBuf, Vec<PathBuf>, Vec<String>) {
|
||||||
|
crate::testlog::init();
|
||||||
|
let root = folder(&format!(
|
||||||
|
"shell-{}",
|
||||||
|
shell.rsplit('/').next().expect("a name")
|
||||||
|
));
|
||||||
|
let (dir, home, start) = (
|
||||||
|
root.join("integration"),
|
||||||
|
root.join("home"),
|
||||||
|
root.join("start"),
|
||||||
|
);
|
||||||
|
let inner = start.join("in ner%");
|
||||||
|
for made in [&home, &inner] {
|
||||||
|
fs::create_dir_all(made).expect("a folder");
|
||||||
|
}
|
||||||
|
install(&dir).expect("scripts install");
|
||||||
|
let mut launch = launch(shell, start.clone(), &dir).expect("a launch");
|
||||||
|
launch.env.push(("HOME".into(), home.display().to_string()));
|
||||||
|
let mut pty = spawn(&launch, (80, 24), (8, 16), 100).expect("the shell starts");
|
||||||
|
let (mut folders, mut titles) = (Vec::new(), Vec::new());
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(20);
|
||||||
|
let mut asked = false;
|
||||||
|
while !folders.contains(&inner) {
|
||||||
|
assert!(
|
||||||
|
Instant::now() < deadline,
|
||||||
|
"{shell} reported {folders:?} and {titles:?} on\n{}",
|
||||||
|
screen(&pty)
|
||||||
|
);
|
||||||
|
match pty.events.try_recv() {
|
||||||
|
Ok(PtyEvent::Folder(folder)) => folders.push(folder),
|
||||||
|
Ok(PtyEvent::Term(Event::Title(title))) => titles.push(title),
|
||||||
|
Ok(PtyEvent::Term(Event::PtyWrite(reply))) => pty.write(reply.into_bytes()),
|
||||||
|
Ok(PtyEvent::Term(Event::ChildExit(status))) => {
|
||||||
|
panic!("{shell} ended, {status}, after {folders:?}")
|
||||||
|
}
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(TryRecvError::Closed) => panic!("{shell}'s events ended"),
|
||||||
|
Err(TryRecvError::Empty) => std::thread::sleep(Duration::from_millis(10)),
|
||||||
|
}
|
||||||
|
if !asked && folders.contains(&start) {
|
||||||
|
pty.write(format!("cd '{}'\r", inner.display()).into_bytes());
|
||||||
|
asked = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pty.write(&b"exit\r"[..]);
|
||||||
|
loop {
|
||||||
|
assert!(
|
||||||
|
Instant::now() < deadline,
|
||||||
|
"{shell} did not exit:\n{}",
|
||||||
|
screen(&pty)
|
||||||
|
);
|
||||||
|
match pty.events.try_recv() {
|
||||||
|
Ok(PtyEvent::Term(Event::ChildExit(_))) | Err(TryRecvError::Closed) => break,
|
||||||
|
Ok(PtyEvent::Term(Event::Title(title))) => titles.push(title),
|
||||||
|
Ok(PtyEvent::Term(Event::PtyWrite(reply))) => pty.write(reply.into_bytes()),
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(TryRecvError::Empty) => std::thread::sleep(Duration::from_millis(10)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(pty);
|
||||||
|
fs::remove_dir_all(&root).expect("cleanup");
|
||||||
|
(start, folders, titles)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The screen's rows, for a failure's message.
|
||||||
|
fn screen(pty: &crate::term::Pty) -> String {
|
||||||
|
use alacritty_terminal::{grid::Dimensions, index::Line};
|
||||||
|
let term = pty.term.lock();
|
||||||
|
(0..term.screen_lines())
|
||||||
|
.map(|line| {
|
||||||
|
let row = &term.grid()[Line(line as i32)];
|
||||||
|
let text: String = row.into_iter().map(|cell| cell.c).collect();
|
||||||
|
text.trim_end().to_string()
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_scripts_install_once_and_mend_a_changed_one() {
|
||||||
|
let dir = folder("install");
|
||||||
|
install(&dir).expect("installs");
|
||||||
|
let zshenv = dir.join("zsh/.zshenv");
|
||||||
|
assert_eq!(fs::read_to_string(&zshenv).expect("written"), SCRIPTS[0].1);
|
||||||
|
fs::write(&zshenv, "changed").expect("write");
|
||||||
|
install(&dir).expect("installs again");
|
||||||
|
assert_eq!(fs::read_to_string(&zshenv).expect("mended"), SCRIPTS[0].1);
|
||||||
|
fs::remove_dir_all(dir).expect("cleanup");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zsh_reports_its_folder_and_title() {
|
||||||
|
let (start, folders, titles) = reports(&installed("zsh"));
|
||||||
|
assert_eq!(folders[0], start);
|
||||||
|
assert!(titles.contains(&"start".to_string()), "{titles:?}");
|
||||||
|
assert!(
|
||||||
|
titles.iter().any(|title| title.starts_with("cd ")),
|
||||||
|
"{titles:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bash_reports_its_folder_and_title() {
|
||||||
|
let (start, folders, titles) = reports(&installed("bash"));
|
||||||
|
assert_eq!(folders[0], start);
|
||||||
|
assert!(titles.contains(&"in ner%".to_string()), "{titles:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fish_reports_its_folder_and_keeps_its_own_title() {
|
||||||
|
let (start, folders, titles) = reports(&installed("fish"));
|
||||||
|
assert_eq!(folders[0], start);
|
||||||
|
assert!(
|
||||||
|
titles.iter().any(|title| title.ends_with("start")),
|
||||||
|
"{titles:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[test]
|
||||||
|
fn powershell_reports_its_folder_and_title() {
|
||||||
|
let (start, folders, titles) = reports(&installed("pwsh"));
|
||||||
|
assert_eq!(folders[0], start);
|
||||||
|
assert!(titles.contains(&"in ner%".to_string()), "{titles:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_unknown_shell_starts_without_integration() {
|
||||||
|
let dir = PathBuf::from("/nonexistent");
|
||||||
|
let launch = launch("/bin/sh", PathBuf::from("/"), &dir).expect("a launch");
|
||||||
|
assert_eq!(launch.program, Some(("/bin/sh".into(), Vec::new())));
|
||||||
|
assert!(launch.env.is_empty());
|
||||||
|
}
|
||||||
@@ -125,6 +125,11 @@ pub(crate) fn spawn(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The user's login shell: SHELL, else the password entry's.
|
||||||
|
pub(crate) fn login_shell_path() -> io::Result<String> {
|
||||||
|
Ok(User::find()?.shell)
|
||||||
|
}
|
||||||
|
|
||||||
/// macOS runs the shell through `login` so it is a login shell on a tty, as Terminal does.
|
/// macOS runs the shell through `login` so it is a login shell on a tty, as Terminal does.
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn login_shell(user: &User) -> Command {
|
fn login_shell(user: &User) -> Command {
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ struct Capture;
|
|||||||
|
|
||||||
impl log::Log for Capture {
|
impl log::Log for Capture {
|
||||||
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
||||||
metadata.level() <= log::Level::Debug
|
metadata.level() <= log::Level::Debug && metadata.target().starts_with("elyterminal")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log(&self, record: &log::Record) {
|
fn log(&self, record: &log::Record) {
|
||||||
|
|||||||
Reference in new issue
Block a user