@microsoft/inshellisense 0.0.1-rc.13 → 0.0.1-rc.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -81,7 +81,7 @@ inshellisense supports the following shells:
81
81
 
82
82
  ## Configuration
83
83
 
84
- All configuration is done through a [toml](https://toml.io/) file located at `~/.inshellisenserc`. The [JSON schema](https://json-schema.org/) for the configuration file can be found [here](https://github.com/microsoft/inshellisense/blob/main/src/utils/config.ts).
84
+ All configuration is done through a [toml](https://toml.io/) file. You can create this file at `~/.inshellisenserc` or, for XDG compliance, at `~/.config/inshellisense/rc.toml`. The [JSON schema](https://json-schema.org/) for the configuration file can be found [here](https://github.com/microsoft/inshellisense/blob/main/src/utils/config.ts).
85
85
 
86
86
  ### Keybindings
87
87
 
@@ -106,18 +106,6 @@ key = "escape"
106
106
 
107
107
  Key names are matched against the Node.js [keypress](https://nodejs.org/api/readline.html#readlineemitkeypresseventsstream-interface) events.
108
108
 
109
- ### Custom Prompts (Windows)
110
-
111
- If you are using a custom prompt in your shell (anything that is not the default PS1), you will need to set up a custom prompt in the inshellisense config file. This is because Windows strips details from your prompt which are required for inshellisense to work. To do this, update your config file in your home directory and add the following configuration:
112
-
113
- ```toml
114
- [[prompt.bash]]
115
- regex = "(?<prompt>^>\\s*)" # the prompt match group will be used to detect the prompt
116
- postfix = ">" # the postfix is the last expected character in your prompt
117
- ```
118
-
119
- This example adds custom prompt detection for bash where the prompt is expected to be only `> `. You can add similar configurations for other shells as well as well as multiple configurations for each shell.
120
-
121
109
  ## Contributing
122
110
 
123
111
  This project welcomes contributions and suggestions. Most contributions require you to agree to a
@@ -12,7 +12,7 @@ export const action = (program) => async (options) => {
12
12
  const inISTerm = process.env.ISTERM === "1";
13
13
  if (options.check || inISTerm) {
14
14
  process.stdout.write(renderConfirmation(inISTerm));
15
- return;
15
+ process.exit(0);
16
16
  }
17
17
  if (options.verbose)
18
18
  await log.enable();
@@ -32,5 +32,5 @@ export const action = (program) => async (options) => {
32
32
  await setupBashPreExec();
33
33
  }
34
34
  await loadAliases(shell);
35
- await render(shell, options.test ?? false);
35
+ await render(shell, options.test ?? false, options.login ?? false);
36
36
  };
@@ -0,0 +1,26 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { Command } from "commander";
4
+ import { loadConfig } from "../../utils/config.js";
5
+ import { getSpecNames, loadLocalSpecsSet } from "../../runtime/runtime.js";
6
+ import { getAliasNames, loadAliases } from "../../runtime/alias.js";
7
+ import { aliasSupportedShells } from "../../utils/shell.js";
8
+ const supportedShells = aliasSupportedShells.join(", ");
9
+ const action = (program) => async (options) => {
10
+ await loadConfig(program);
11
+ await loadLocalSpecsSet();
12
+ const { shell } = options;
13
+ if (shell && !aliasSupportedShells.map((s) => s.valueOf()).includes(shell)) {
14
+ program.error(`Unsupported shell: '${shell}', supported shells: ${supportedShells}`, { exitCode: 1 });
15
+ }
16
+ if (shell) {
17
+ await loadAliases(shell);
18
+ }
19
+ process.stdout.write(JSON.stringify([...getAliasNames(), ...getSpecNames()]));
20
+ process.exit(0);
21
+ };
22
+ const cmd = new Command("list");
23
+ cmd.description(`list the names of all available specs`);
24
+ cmd.option("-s, --shell <shell>", `shell to use alias specs, supported shells: ${supportedShells}`);
25
+ cmd.action(action(cmd));
26
+ export default cmd;
@@ -0,0 +1,8 @@
1
+ // Copyright (c) Microsoft Corporation.
2
+ // Licensed under the MIT License.
3
+ import { Command } from "commander";
4
+ import list from "./list.js";
5
+ const cmd = new Command("specs");
6
+ cmd.description(`manage specs`);
7
+ cmd.addCommand(list);
8
+ export default cmd;
package/build/index.js CHANGED
@@ -6,6 +6,7 @@ import { Command, Option } from "commander";
6
6
  import complete from "./commands/complete.js";
7
7
  import uninstall from "./commands/uninstall.js";
8
8
  import init from "./commands/init.js";
9
+ import specs from "./commands/specs/root.js";
9
10
  import { action, supportedShells } from "./commands/root.js";
10
11
  import { getVersion } from "./utils/version.js";
11
12
  const program = new Command();
@@ -19,12 +20,15 @@ program
19
20
  .description("IDE style command line auto complete")
20
21
  .version(await getVersion(), "-v, --version", "output the current version")
21
22
  .action(action(program))
23
+ .option("-l, --login", `start shell as a login shell`)
22
24
  .option("-s, --shell <shell>", `shell to use for command execution, supported shells: ${supportedShells}`)
23
25
  .option("-c, --check", `check if shell is in an inshellisense session`)
24
26
  .addOption(hiddenOption("-T, --test", "used to make e2e tests reproducible across machines"))
25
27
  .option("-V, --verbose", `enable verbose logging`)
26
- .showHelpAfterError("(add --help for additional information)");
28
+ .showHelpAfterError("(add --help for additional information)")
29
+ .passThroughOptions();
27
30
  program.addCommand(complete);
28
31
  program.addCommand(uninstall);
29
32
  program.addCommand(init);
33
+ program.addCommand(specs);
30
34
  program.parse();
@@ -4,19 +4,21 @@ import convert from "color-convert";
4
4
  import os from "node:os";
5
5
  import { getShellPromptRewrites, Shell } from "../utils/shell.js";
6
6
  import log from "../utils/log.js";
7
- import { getConfig } from "../utils/config.js";
8
7
  const maxPromptPollDistance = 10;
9
8
  export class CommandManager {
10
9
  #activeCommand;
11
10
  #terminal;
12
11
  #previousCommandLines;
12
+ #maxCursorY;
13
13
  #shell;
14
14
  #promptRewrites;
15
15
  #supportsProperOscPlacements = os.platform() !== "win32";
16
+ promptTerminator = "";
16
17
  constructor(terminal, shell) {
17
18
  this.#terminal = terminal;
18
19
  this.#shell = shell;
19
20
  this.#activeCommand = {};
21
+ this.#maxCursorY = 0;
20
22
  this.#previousCommandLines = new Set();
21
23
  this.#promptRewrites = getShellPromptRewrites(shell);
22
24
  if (this.#supportsProperOscPlacements) {
@@ -45,17 +47,9 @@ export class CommandManager {
45
47
  }
46
48
  handleClear() {
47
49
  this.handlePromptStart();
50
+ this.#maxCursorY = 0;
48
51
  this.#previousCommandLines = new Set();
49
52
  }
50
- _extractPrompt(lineText, patterns) {
51
- for (const { regex, postfix } of patterns) {
52
- const customPrompt = lineText.match(new RegExp(regex))?.groups?.prompt;
53
- const adjustedPrompt = this._adjustPrompt(customPrompt, lineText, postfix);
54
- if (adjustedPrompt) {
55
- return adjustedPrompt;
56
- }
57
- }
58
- }
59
53
  _getWindowsPrompt(y) {
60
54
  const line = this.#terminal.buffer.active.getLine(y);
61
55
  if (!line) {
@@ -65,15 +59,16 @@ export class CommandManager {
65
59
  if (!lineText) {
66
60
  return;
67
61
  }
62
+ // dynamic prompt terminator
63
+ if (this.promptTerminator && lineText.trim().endsWith(this.promptTerminator)) {
64
+ const adjustedPrompt = this._adjustPrompt(lineText, lineText, this.promptTerminator);
65
+ if (adjustedPrompt) {
66
+ return adjustedPrompt;
67
+ }
68
+ }
68
69
  // User defined prompt
69
- const inshellisenseConfig = getConfig();
70
70
  if (this.#shell == Shell.Bash) {
71
- if (inshellisenseConfig?.prompt?.bash != null) {
72
- const extractedPrompt = this._extractPrompt(lineText, inshellisenseConfig.prompt.bash);
73
- if (extractedPrompt)
74
- return extractedPrompt;
75
- }
76
- const bashPrompt = lineText.match(/^(?<prompt>.*\$\s?)/)?.groups?.prompt;
71
+ const bashPrompt = lineText.match(/^(?<prompt>\$\s?)/)?.groups?.prompt;
77
72
  if (bashPrompt) {
78
73
  const adjustedPrompt = this._adjustPrompt(bashPrompt, lineText, "$");
79
74
  if (adjustedPrompt) {
@@ -81,12 +76,16 @@ export class CommandManager {
81
76
  }
82
77
  }
83
78
  }
84
- if (this.#shell == Shell.Nushell) {
85
- if (inshellisenseConfig?.prompt?.nu != null) {
86
- const extractedPrompt = this._extractPrompt(lineText, inshellisenseConfig.prompt.nu);
87
- if (extractedPrompt)
88
- return extractedPrompt;
79
+ if (this.#shell == Shell.Fish) {
80
+ const fishPrompt = lineText.match(/(?<prompt>.*>\s?)/)?.groups?.prompt;
81
+ if (fishPrompt) {
82
+ const adjustedPrompt = this._adjustPrompt(fishPrompt, lineText, ">");
83
+ if (adjustedPrompt) {
84
+ return adjustedPrompt;
85
+ }
89
86
  }
87
+ }
88
+ if (this.#shell == Shell.Nushell) {
90
89
  const nushellPrompt = lineText.match(/(?<prompt>.*>\s?)/)?.groups?.prompt;
91
90
  if (nushellPrompt) {
92
91
  const adjustedPrompt = this._adjustPrompt(nushellPrompt, lineText, ">");
@@ -96,11 +95,6 @@ export class CommandManager {
96
95
  }
97
96
  }
98
97
  if (this.#shell == Shell.Xonsh) {
99
- if (inshellisenseConfig?.prompt?.xonsh != null) {
100
- const extractedPrompt = this._extractPrompt(lineText, inshellisenseConfig.prompt.xonsh);
101
- if (extractedPrompt)
102
- return extractedPrompt;
103
- }
104
98
  let xonshPrompt = lineText.match(/(?<prompt>.*@\s?)/)?.groups?.prompt;
105
99
  if (xonshPrompt) {
106
100
  const adjustedPrompt = this._adjustPrompt(xonshPrompt, lineText, "@");
@@ -117,16 +111,6 @@ export class CommandManager {
117
111
  }
118
112
  }
119
113
  if (this.#shell == Shell.Powershell || this.#shell == Shell.Pwsh) {
120
- if (inshellisenseConfig?.prompt?.powershell != null) {
121
- const extractedPrompt = this._extractPrompt(lineText, inshellisenseConfig.prompt.powershell);
122
- if (extractedPrompt)
123
- return extractedPrompt;
124
- }
125
- if (inshellisenseConfig?.prompt?.pwsh != null) {
126
- const extractedPrompt = this._extractPrompt(lineText, inshellisenseConfig.prompt.pwsh);
127
- if (extractedPrompt)
128
- return extractedPrompt;
129
- }
130
114
  const pwshPrompt = lineText.match(/(?<prompt>(\(.+\)\s)?(?:PS.+>\s?))/)?.groups?.prompt;
131
115
  if (pwshPrompt) {
132
116
  const adjustedPrompt = this._adjustPrompt(pwshPrompt, lineText, ">");
@@ -188,15 +172,84 @@ export class CommandManager {
188
172
  cursorTerminated: this.#activeCommand.cursorTerminated,
189
173
  };
190
174
  }
175
+ clearActiveCommand() {
176
+ this.#activeCommand = {};
177
+ }
178
+ _getCommandLines() {
179
+ const lines = [];
180
+ let lineY = this.#activeCommand.promptEndMarker.line;
181
+ let line = this.#terminal.buffer.active.getLine(this.#activeCommand.promptEndMarker.line);
182
+ const absoluteY = this.#terminal.buffer.active.baseY + this.#terminal.buffer.active.cursorY;
183
+ for (; lineY < this.#terminal.buffer.active.baseY + this.#terminal.rows;) {
184
+ if (line)
185
+ lines.push(line);
186
+ lineY += 1;
187
+ line = this.#terminal.buffer.active.getLine(lineY);
188
+ const lineWrapped = line?.isWrapped;
189
+ const cursorWrapped = absoluteY > lineY - 1;
190
+ const wrapped = lineWrapped || cursorWrapped;
191
+ if (!wrapped)
192
+ break;
193
+ }
194
+ return lines;
195
+ }
196
+ _getCommandText(commandLines) {
197
+ const absoluteY = this.#terminal.buffer.active.baseY + this.#terminal.buffer.active.cursorY;
198
+ const cursorLine = Math.max(absoluteY - this.#activeCommand.promptEndMarker.line, 0);
199
+ let preCursorCommand = "";
200
+ let postCursorCommand = "";
201
+ let suggestion = "";
202
+ for (const [y, line] of commandLines.entries()) {
203
+ const startX = y == 0 ? this.#activeCommand.promptText?.length ?? 0 : 0;
204
+ for (let x = startX; x < this.#terminal.cols; x++) {
205
+ if (postCursorCommand.endsWith(" "))
206
+ break; // assume that a command that ends with 4 spaces is terminated, avoids capturing right prompts
207
+ const cell = line.getCell(x);
208
+ if (cell == null)
209
+ continue;
210
+ const chars = cell.getChars() == "" ? " " : cell.getChars();
211
+ const beforeCursor = y < cursorLine || (y == cursorLine && x < this.#terminal.buffer.active.cursorX);
212
+ const isCommand = !this._isSuggestion(cell) && suggestion.length == 0;
213
+ if (isCommand && beforeCursor) {
214
+ preCursorCommand += chars;
215
+ }
216
+ else if (isCommand) {
217
+ postCursorCommand += chars;
218
+ }
219
+ else {
220
+ suggestion += chars;
221
+ }
222
+ }
223
+ }
224
+ log.debug({ msg: "command text", preCursorCommand, postCursorCommand, suggestion });
225
+ return { suggestion, preCursorCommand, postCursorCommand };
226
+ }
227
+ _getCommandOutputStatus(commandLines) {
228
+ const outputLineY = this.#activeCommand.promptEndMarker.line + commandLines;
229
+ const maxLineY = this.#terminal.buffer.active.baseY + this.#terminal.rows;
230
+ if (outputLineY >= maxLineY)
231
+ return false;
232
+ const line = this.#terminal.buffer.active.getLine(outputLineY);
233
+ let cell = undefined;
234
+ for (let i = 0; i < this.#terminal.cols; i++) {
235
+ cell = line?.getCell(i, cell);
236
+ if (cell?.getChars() != "") {
237
+ return true;
238
+ }
239
+ }
240
+ return false;
241
+ }
191
242
  termSync() {
192
243
  if (this.#activeCommand.promptEndMarker == null || this.#activeCommand.promptStartMarker == null) {
193
244
  return;
194
245
  }
195
246
  const globalCursorPosition = this.#terminal.buffer.active.baseY + this.#terminal.buffer.active.cursorY;
196
247
  const withinPollDistance = globalCursorPosition < this.#activeCommand.promptEndMarker.line + 5;
197
- if (globalCursorPosition < this.#activeCommand.promptStartMarker.line) {
248
+ this.#maxCursorY = Math.max(this.#maxCursorY, globalCursorPosition);
249
+ if (globalCursorPosition < this.#activeCommand.promptStartMarker.line || globalCursorPosition < this.#maxCursorY) {
198
250
  this.handleClear();
199
251
  this.#activeCommand.promptEndMarker = this.#terminal.registerMarker(0);
252
+ return;
200
253
  }
201
254
  if (this.#activeCommand.promptEndMarker == null)
202
255
  return;
@@ -217,60 +270,15 @@ export class CommandManager {
217
270
  }
218
271
  // if the prompt is set, now parse out the values from the terminal
219
272
  if (this.#activeCommand.promptText != null) {
220
- let lineY = this.#activeCommand.promptEndMarker.line;
221
- let line = this.#terminal.buffer.active.getLine(this.#activeCommand.promptEndMarker.line);
222
- let command = "";
223
- let wrappedCommand = "";
224
- let suggestions = "";
225
- let isWrapped = false;
226
- for (; lineY < this.#terminal.buffer.active.baseY + this.#terminal.rows;) {
227
- for (let i = lineY == this.#activeCommand.promptEndMarker.line ? this.#activeCommand.promptText.length : 0; i < this.#terminal.cols; i++) {
228
- if (command.endsWith(" "))
229
- break; // assume that a command that ends with 4 spaces is terminated, avoids capturing right prompts
230
- const cell = line?.getCell(i);
231
- if (cell == null)
232
- continue;
233
- const chars = cell.getChars();
234
- const cleanedChars = chars == "" ? " " : chars;
235
- if (!this._isSuggestion(cell) && suggestions.length == 0) {
236
- command += cleanedChars;
237
- wrappedCommand += cleanedChars;
238
- }
239
- else {
240
- suggestions += cleanedChars;
241
- }
242
- }
243
- lineY += 1;
244
- line = this.#terminal.buffer.active.getLine(lineY);
245
- const wrapped = line?.isWrapped || this.#terminal.buffer.active.cursorY + this.#terminal.buffer.active.baseY != lineY - 1;
246
- isWrapped = isWrapped || wrapped;
247
- if (!wrapped) {
248
- break;
249
- }
250
- wrappedCommand = "";
251
- }
252
- const cursorAtEndOfInput = isWrapped
253
- ? wrappedCommand.trim().length % this.#terminal.cols <= this.#terminal.buffer.active.cursorX
254
- : (this.#activeCommand.promptText.length + command.trimEnd().length) % this.#terminal.cols <= this.#terminal.buffer.active.cursorX;
255
- let hasOutput = false;
256
- let cell = undefined;
257
- for (let i = 0; i < this.#terminal.cols; i++) {
258
- cell = line?.getCell(i, cell);
259
- if (cell == null)
260
- continue;
261
- hasOutput = cell.getChars() != "";
262
- if (hasOutput) {
263
- break;
264
- }
265
- }
266
- const postfixActive = isWrapped
267
- ? wrappedCommand.trim().length < this.#terminal.buffer.active.cursorX
268
- : this.#activeCommand.promptText.length + command.trimEnd().length < this.#terminal.buffer.active.cursorX;
269
- const commandPostfix = postfixActive ? " " : "";
273
+ const commandLines = this._getCommandLines();
274
+ const { suggestion, preCursorCommand, postCursorCommand } = this._getCommandText(commandLines);
275
+ const command = preCursorCommand + postCursorCommand.trim();
276
+ const cursorAtEndOfInput = postCursorCommand.trim() == "";
277
+ const hasOutput = this._getCommandOutputStatus(commandLines.length);
270
278
  this.#activeCommand.persistentOutput = this.#activeCommand.hasOutput && hasOutput;
271
279
  this.#activeCommand.hasOutput = hasOutput;
272
- this.#activeCommand.suggestionsText = suggestions.trim();
273
- this.#activeCommand.commandText = command.trim() + commandPostfix;
280
+ this.#activeCommand.suggestionsText = suggestion;
281
+ this.#activeCommand.commandText = command;
274
282
  this.#activeCommand.cursorTerminated = cursorAtEndOfInput;
275
283
  }
276
284
  log.debug({
@@ -278,6 +286,8 @@ export class CommandManager {
278
286
  ...this.#activeCommand,
279
287
  promptEndMarker: this.#activeCommand.promptEndMarker?.line,
280
288
  promptStartMarker: this.#activeCommand.promptStartMarker?.line,
289
+ cursorX: this.#terminal.buffer.active.cursorX,
290
+ cursorY: globalCursorPosition,
281
291
  });
282
292
  }
283
293
  }
@@ -6,6 +6,7 @@ import os from "node:os";
6
6
  import path from "node:path";
7
7
  import url from "node:url";
8
8
  import fs from "node:fs";
9
+ import stripAnsi from "strip-ansi";
9
10
  import pty from "@homebridge/node-pty-prebuilt-multiarch";
10
11
  import { Shell, userZdotdir, zdotdir } from "../utils/shell.js";
11
12
  import { IsTermOscPs, IstermOscPt, IstermPromptStart, IstermPromptEnd } from "../utils/ansi.js";
@@ -13,8 +14,8 @@ import xterm from "@xterm/headless";
13
14
  import { CommandManager } from "./commandManager.js";
14
15
  import log from "../utils/log.js";
15
16
  import { gitBashPath } from "../utils/shell.js";
16
- import ansi from "ansi-escapes";
17
17
  import styles from "ansi-styles";
18
+ import * as ansi from "../utils/ansi.js";
18
19
  const ISTermOnDataEvent = "data";
19
20
  export class ISTerm {
20
21
  pid;
@@ -31,13 +32,13 @@ export class ISTerm {
31
32
  #term;
32
33
  #commandManager;
33
34
  #shell;
34
- constructor({ shell, cols, rows, env, shellTarget, shellArgs, underTest }) {
35
+ constructor({ shell, cols, rows, env, shellTarget, shellArgs, underTest, login }) {
35
36
  this.#pty = pty.spawn(shellTarget, shellArgs ?? [], {
36
37
  name: "xterm-256color",
37
38
  cols,
38
39
  rows,
39
40
  cwd: process.cwd(),
40
- env: { ...convertToPtyEnv(shell, underTest), ...env },
41
+ env: { ...convertToPtyEnv(shell, underTest, login), ...env },
41
42
  });
42
43
  this.pid = this.#pty.pid;
43
44
  this.cols = this.#pty.cols;
@@ -84,6 +85,9 @@ export class ISTerm {
84
85
  }
85
86
  return cwd;
86
87
  }
88
+ _sanitizedPrompt(prompt) {
89
+ return stripAnsi(prompt);
90
+ }
87
91
  _handleIsSequence(data) {
88
92
  const argsIndex = data.indexOf(";");
89
93
  const sequence = argsIndex === -1 ? data : data.substring(0, argsIndex);
@@ -101,6 +105,19 @@ export class ISTerm {
101
105
  }
102
106
  break;
103
107
  }
108
+ case IstermOscPt.Prompt: {
109
+ const prompt = data.split(";").slice(1).join(";");
110
+ if (prompt != null) {
111
+ const sanitizedPrompt = this._sanitizedPrompt(this._deserializeIsMessage(prompt));
112
+ const lastPromptLine = sanitizedPrompt.substring(sanitizedPrompt.lastIndexOf("\n")).trim();
113
+ const promptTerminator = lastPromptLine.substring(lastPromptLine.lastIndexOf(" ")).trim();
114
+ if (promptTerminator) {
115
+ this.#commandManager.promptTerminator = promptTerminator;
116
+ log.debug({ msg: "prompt terminator", promptTerminator });
117
+ }
118
+ }
119
+ break;
120
+ }
104
121
  default:
105
122
  return false;
106
123
  }
@@ -144,24 +161,37 @@ export class ISTerm {
144
161
  };
145
162
  }
146
163
  _sameAccent(baseCell, targetCell) {
147
- return baseCell?.isBold() == targetCell?.isBold() && baseCell?.isItalic() == targetCell?.isItalic() && baseCell?.isUnderline() == targetCell?.isUnderline();
164
+ return (baseCell?.isBold() == targetCell?.isBold() &&
165
+ baseCell?.isItalic() == targetCell?.isItalic() &&
166
+ baseCell?.isUnderline() == targetCell?.isUnderline() &&
167
+ baseCell?.extended.underlineStyle == targetCell?.extended.underlineStyle &&
168
+ baseCell?.hasExtendedAttrs() == targetCell?.hasExtendedAttrs() &&
169
+ baseCell?.isInverse() == targetCell?.isInverse() &&
170
+ baseCell?.isBlink() == targetCell?.isBlink() &&
171
+ baseCell?.isInvisible() == targetCell?.isInvisible() &&
172
+ baseCell?.isDim() == targetCell?.isDim() &&
173
+ baseCell?.isStrikethrough() == targetCell?.isStrikethrough());
148
174
  }
149
175
  _getAnsiAccents(cell) {
150
176
  if (cell == null)
151
177
  return "";
152
- let boldAnsi = "";
153
- if (cell.isBold()) {
154
- boldAnsi = "\x1b[1m";
155
- }
156
- let italicAnsi = "";
157
- if (cell.isItalic()) {
158
- italicAnsi = "\x1b[3m";
159
- }
160
178
  let underlineAnsi = "";
161
179
  if (cell.isUnderline()) {
162
- underlineAnsi = "\x1b[4m";
180
+ if (cell.hasExtendedAttrs() && cell.extended.underlineStyle) {
181
+ underlineAnsi = `\x1b[4:${cell.extended.underlineStyle}m`;
182
+ }
183
+ else {
184
+ underlineAnsi = "\x1b[4m";
185
+ }
163
186
  }
164
- return boldAnsi + italicAnsi + underlineAnsi;
187
+ const boldAnsi = cell.isBold() ? "\x1b[1m" : "";
188
+ const dimAnsi = cell.isDim() ? "\x1b[2m" : "";
189
+ const italicAnsi = cell.isItalic() ? "\x1b[3m" : "";
190
+ const blinkAnsi = cell.isBlink() ? "\x1b[5m" : "";
191
+ const inverseAnsi = cell.isInverse() ? "\x1b[7m" : "";
192
+ const invisibleAnsi = cell.isInvisible() ? "\x1b[8m" : "";
193
+ const strikethroughAnsi = cell.isStrikethrough() ? "\x1b[9m" : "";
194
+ return boldAnsi + italicAnsi + underlineAnsi + inverseAnsi + dimAnsi + blinkAnsi + invisibleAnsi + strikethroughAnsi;
165
195
  }
166
196
  _sameColor(baseCell, targetCell) {
167
197
  return (baseCell?.getBgColorMode() == targetCell?.getBgColorMode() &&
@@ -196,24 +226,32 @@ export class ISTerm {
196
226
  }
197
227
  return bgAnsi + fgAnsi;
198
228
  }
229
+ clearCommand() {
230
+ this.#commandManager.clearActiveCommand();
231
+ }
199
232
  getCells(height, direction) {
200
233
  const currentCursorPosition = this.#term.buffer.active.cursorY + this.#term.buffer.active.baseY;
201
234
  const writeLine = (y) => {
202
235
  const line = this.#term.buffer.active.getLine(y);
203
- const ansiLine = ["\x1b[0m"];
236
+ const ansiLine = [ansi.resetColor, ansi.resetLine];
204
237
  if (line == null)
205
238
  return "";
206
239
  let prevCell;
207
240
  for (let x = 0; x < line.length; x++) {
208
241
  const cell = line.getCell(x);
209
242
  const chars = cell?.getChars() ?? "";
210
- if (!this._sameColor(prevCell, cell)) {
243
+ const sameColor = this._sameColor(prevCell, cell);
244
+ const sameAccents = this._sameAccent(prevCell, cell);
245
+ if (!sameColor || !sameAccents) {
246
+ ansiLine.push(ansi.resetColor);
247
+ }
248
+ if (!sameColor) {
211
249
  ansiLine.push(this._getAnsiColors(cell));
212
250
  }
213
- if (!this._sameAccent(prevCell, cell)) {
251
+ if (!sameAccents) {
214
252
  ansiLine.push(this._getAnsiAccents(cell));
215
253
  }
216
- ansiLine.push(chars == "" ? " " : chars);
254
+ ansiLine.push(chars == "" ? ansi.cursorForward() : chars);
217
255
  prevCell = cell;
218
256
  }
219
257
  return ansiLine.join("");
@@ -237,10 +275,10 @@ export class ISTerm {
237
275
  }
238
276
  }
239
277
  export const spawn = async (options) => {
240
- const { shellTarget, shellArgs } = await convertToPtyTarget(options.shell, options.underTest);
278
+ const { shellTarget, shellArgs } = await convertToPtyTarget(options.shell, options.underTest, options.login);
241
279
  return new ISTerm({ ...options, shellTarget, shellArgs });
242
280
  };
243
- const convertToPtyTarget = async (shell, underTest) => {
281
+ const convertToPtyTarget = async (shell, underTest, login) => {
244
282
  const platform = os.platform();
245
283
  const shellTarget = shell == Shell.Bash && platform == "win32" ? await gitBashPath() : platform == "win32" ? `${shell}.exe` : shell;
246
284
  const shellFolderPath = path.join(path.dirname(url.fileURLToPath(import.meta.url)), "..", "..", "shell");
@@ -254,7 +292,10 @@ const convertToPtyTarget = async (shell, underTest) => {
254
292
  shellArgs = ["-noexit", "-command", `try { . "${path.join(shellFolderPath, "shellIntegration.ps1")}" } catch {}`];
255
293
  break;
256
294
  case Shell.Fish:
257
- shellArgs = ["--init-command", `. ${path.join(shellFolderPath, "shellIntegration.fish").replace(/(\s+)/g, "\\$1")}`];
295
+ shellArgs =
296
+ platform == "win32"
297
+ ? ["--init-command", `. "$(cygpath -u '${path.join(shellFolderPath, "shellIntegration.fish")}')"`]
298
+ : ["--init-command", `. ${path.join(shellFolderPath, "shellIntegration.fish").replace(/(\s+)/g, "\\$1")}`];
258
299
  break;
259
300
  case Shell.Xonsh: {
260
301
  const sharedConfig = os.platform() == "win32" ? path.join("C:\\ProgramData", "xonsh", "xonshrc") : path.join("etc", "xonsh", "xonshrc");
@@ -273,15 +314,31 @@ const convertToPtyTarget = async (shell, underTest) => {
273
314
  shellArgs.push("-n");
274
315
  break;
275
316
  }
317
+ if (login) {
318
+ switch (shell) {
319
+ case Shell.Powershell:
320
+ case Shell.Pwsh:
321
+ shellArgs.unshift("-login");
322
+ break;
323
+ case Shell.Zsh:
324
+ case Shell.Fish:
325
+ case Shell.Xonsh:
326
+ case Shell.Nushell:
327
+ shellArgs.unshift("--login");
328
+ break;
329
+ }
330
+ }
276
331
  return { shellTarget, shellArgs };
277
332
  };
278
- const convertToPtyEnv = (shell, underTest) => {
333
+ const convertToPtyEnv = (shell, underTest, login) => {
279
334
  const env = {
280
335
  ...process.env,
281
336
  ISTERM: "1",
282
337
  };
283
338
  if (underTest)
284
339
  env.ISTERM_TESTING = "1";
340
+ if (login)
341
+ env.ISTERM_LOGIN = "1";
285
342
  switch (shell) {
286
343
  case Shell.Cmd: {
287
344
  if (underTest) {
@@ -48,6 +48,7 @@ export const loadAliases = async (shell) => {
48
48
  }
49
49
  return [];
50
50
  };
51
+ export const getAliasNames = () => Object.keys(loadedAliases);
51
52
  export const aliasExpand = (command) => {
52
53
  if (!command.at(0)?.complete)
53
54
  return command;
@@ -103,6 +103,9 @@ export const getSuggestions = async (cmd, cwd, shell) => {
103
103
  }
104
104
  return { ...result, charactersToDrop };
105
105
  };
106
+ export const getSpecNames = () => {
107
+ return Object.keys(specSet).filter((spec) => !spec.startsWith("@") && spec != "-");
108
+ };
106
109
  const getPersistentOptions = (persistentOptions, options) => {
107
110
  const persistentOptionNames = new Set(persistentOptions.map((o) => (typeof o.name === "string" ? [o.name] : o.name)).flat());
108
111
  return persistentOptions.concat((options ?? []).filter((o) => (typeof o.name == "string" ? !persistentOptionNames.has(o.name) : o.name.some((n) => !persistentOptionNames.has(n))) && o.isPersistent === true));
@@ -19,6 +19,7 @@ export class SuggestionManager {
19
19
  #activeSuggestionIdx;
20
20
  #suggestBlob;
21
21
  #shell;
22
+ #hideSuggestions = false;
22
23
  constructor(terminal, shell) {
23
24
  this.#term = terminal;
24
25
  this.#suggestBlob = { suggestions: [] };
@@ -28,7 +29,7 @@ export class SuggestionManager {
28
29
  }
29
30
  async _loadSuggestions() {
30
31
  const commandText = this.#term.getCommandState().commandText;
31
- if (!commandText) {
32
+ if (!commandText || this.#hideSuggestions) {
32
33
  this.#suggestBlob = undefined;
33
34
  this.#activeSuggestionIdx = 0;
34
35
  return;
@@ -122,12 +123,20 @@ export class SuggestionManager {
122
123
  }
123
124
  update(keyPress) {
124
125
  const { name, shift, ctrl } = keyPress;
126
+ if (name == "return") {
127
+ this.#term.clearCommand(); // clear the current command on enter
128
+ }
129
+ // if suggestions are hidden, keep them hidden until during command navigation
130
+ if (this.#hideSuggestions) {
131
+ this.#hideSuggestions = name == "up" || name == "down";
132
+ }
125
133
  if (!this.#suggestBlob) {
126
134
  return false;
127
135
  }
128
136
  const { dismissSuggestions: { key: dismissKey, shift: dismissShift, control: dismissCtrl }, acceptSuggestion: { key: acceptKey, shift: acceptShift, control: acceptCtrl }, nextSuggestion: { key: nextKey, shift: nextShift, control: nextCtrl }, previousSuggestion: { key: prevKey, shift: prevShift, control: prevCtrl }, } = getConfig().bindings;
129
137
  if (name == dismissKey && shift == !!dismissShift && ctrl == !!dismissCtrl) {
130
138
  this.#suggestBlob = undefined;
139
+ this.#hideSuggestions = true;
131
140
  }
132
141
  else if (name == prevKey && shift == !!prevShift && ctrl == !!prevCtrl) {
133
142
  this.#activeSuggestionIdx = Math.max(0, this.#activeSuggestionIdx - 1);
@@ -12,8 +12,8 @@ export const renderConfirmation = (live) => {
12
12
  const statusMessage = live ? chalk.green("live") : chalk.red("not found");
13
13
  return `inshellisense session [${statusMessage}]\n`;
14
14
  };
15
- export const render = async (shell, underTest) => {
16
- const term = await isterm.spawn({ shell, rows: process.stdout.rows, cols: process.stdout.columns, underTest });
15
+ export const render = async (shell, underTest, login) => {
16
+ const term = await isterm.spawn({ shell, rows: process.stdout.rows, cols: process.stdout.columns, underTest, login });
17
17
  const suggestionManager = new SuggestionManager(term, shell);
18
18
  let hasActiveSuggestions = false;
19
19
  let previousSuggestionsRows = 0;
package/build/ui/utils.js CHANGED
@@ -1,6 +1,7 @@
1
1
  // Copyright (c) Microsoft Corporation.
2
2
  // Licensed under the MIT License.
3
3
  import ansi from "ansi-escapes";
4
+ import { resetColor } from "../utils/ansi.js";
4
5
  import wrapAnsi from "wrap-ansi";
5
6
  import chalk from "chalk";
6
7
  import wcwidth from "wcwidth";
@@ -12,7 +13,7 @@ import wcwidth from "wcwidth";
12
13
  */
13
14
  export const renderBox = (rows, width, x, borderColor) => {
14
15
  const result = [];
15
- const setColor = (text) => (borderColor ? chalk.hex(borderColor).apply(text) : text);
16
+ const setColor = (text) => resetColor + (borderColor ? chalk.hex(borderColor).apply(text) : text);
16
17
  result.push(ansi.cursorTo(x) + setColor("┌" + "─".repeat(width - 2) + "┐") + ansi.cursorTo(x));
17
18
  rows.forEach((row) => {
18
19
  result.push(ansi.cursorDown() + setColor("│") + row + setColor("│") + ansi.cursorTo(x));
@@ -11,6 +11,7 @@ export var IstermOscPt;
11
11
  IstermOscPt["PromptStarted"] = "PS";
12
12
  IstermOscPt["PromptEnded"] = "PE";
13
13
  IstermOscPt["CurrentWorkingDirectory"] = "CWD";
14
+ IstermOscPt["Prompt"] = "PROMPT";
14
15
  })(IstermOscPt || (IstermOscPt = {}));
15
16
  export const IstermPromptStart = IS_OSC + IstermOscPt.PromptStarted + BEL;
16
17
  export const IstermPromptEnd = IS_OSC + IstermOscPt.PromptEnded + BEL;
@@ -18,7 +19,10 @@ export const cursorHide = CSI + "?25l";
18
19
  export const cursorShow = CSI + "?25h";
19
20
  export const cursorNextLine = CSI + "E";
20
21
  export const eraseLine = CSI + "2K";
22
+ export const resetColor = CSI + "0m";
23
+ export const resetLine = CSI + "2K";
21
24
  export const cursorBackward = (count = 1) => CSI + count + "D";
25
+ export const cursorForward = (count = 1) => CSI + count + "C";
22
26
  export const cursorTo = ({ x, y }) => {
23
27
  return CSI + (y ?? "") + ";" + (x ?? "") + "H";
24
28
  };
@@ -49,6 +49,7 @@ const configSchema = {
49
49
  acceptSuggestion: bindingSchema,
50
50
  },
51
51
  },
52
+ // DEPRECATED: prompt patterns are no longer used
52
53
  prompt: {
53
54
  type: "object",
54
55
  nullable: true,
@@ -58,6 +59,7 @@ const configSchema = {
58
59
  powershell: promptPatternsSchema,
59
60
  xonsh: promptPatternsSchema,
60
61
  nu: promptPatternsSchema,
62
+ fish: promptPatternsSchema,
61
63
  },
62
64
  },
63
65
  specs: {
@@ -70,9 +72,12 @@ const configSchema = {
70
72
  },
71
73
  additionalProperties: false,
72
74
  };
73
- const configFile = ".inshellisenserc";
75
+ const rcFile = ".inshellisenserc";
76
+ const xdgFile = "rc.toml";
74
77
  const cachePath = path.join(os.homedir(), ".inshellisense");
75
- const configPath = path.join(os.homedir(), configFile);
78
+ const rcPath = path.join(os.homedir(), rcFile);
79
+ const xdgPath = path.join(os.homedir(), ".config", "inshellisense", xdgFile);
80
+ const configPaths = [rcPath, xdgPath];
76
81
  let globalConfig = {
77
82
  bindings: {
78
83
  nextSuggestion: { key: "down" },
@@ -83,38 +88,42 @@ let globalConfig = {
83
88
  };
84
89
  export const getConfig = () => globalConfig;
85
90
  export const loadConfig = async (program) => {
86
- if (fs.existsSync(configPath)) {
87
- let config;
88
- try {
89
- config = toml.parse((await fsAsync.readFile(configPath)).toString());
90
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
+ configPaths.forEach(async (configPath) => {
92
+ if (fs.existsSync(configPath)) {
93
+ let config;
94
+ try {
95
+ config = toml.parse((await fsAsync.readFile(configPath)).toString());
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ }
98
+ catch (e) {
99
+ program.error(`${configPath} is invalid toml. Parsing error on line ${e.line}, column ${e.column}: ${e.message}`);
100
+ }
101
+ const isValid = ajv.validate(configSchema, config);
102
+ if (!isValid) {
103
+ program.error(`${configPath} is invalid: ${ajv.errorsText()}`);
104
+ }
105
+ globalConfig = {
106
+ bindings: {
107
+ nextSuggestion: config?.bindings?.nextSuggestion ?? globalConfig.bindings.nextSuggestion,
108
+ previousSuggestion: config?.bindings?.previousSuggestion ?? globalConfig.bindings.previousSuggestion,
109
+ acceptSuggestion: config?.bindings?.acceptSuggestion ?? globalConfig.bindings.acceptSuggestion,
110
+ dismissSuggestions: config?.bindings?.dismissSuggestions ?? globalConfig.bindings.dismissSuggestions,
111
+ },
112
+ prompt: {
113
+ bash: config.prompt?.bash ?? globalConfig?.prompt?.bash,
114
+ powershell: config.prompt?.powershell ?? globalConfig?.prompt?.powershell,
115
+ xonsh: config.prompt?.xonsh ?? globalConfig?.prompt?.xonsh,
116
+ pwsh: config.prompt?.pwsh ?? globalConfig?.prompt?.pwsh,
117
+ nu: config.prompt?.nu ?? globalConfig?.prompt?.nu,
118
+ fish: config.prompt?.fish ?? globalConfig?.prompt?.fish,
119
+ },
120
+ specs: {
121
+ path: [...(config?.specs?.path ?? []), ...(config?.specs?.path ?? [])],
122
+ },
123
+ };
91
124
  }
92
- catch (e) {
93
- program.error(`${configFile} is invalid toml. Parsing error on line ${e.line}, column ${e.column}: ${e.message}`);
94
- }
95
- const isValid = ajv.validate(configSchema, config);
96
- if (!isValid) {
97
- program.error(`${configFile} is invalid: ${ajv.errorsText()}`);
98
- }
99
- globalConfig = {
100
- bindings: {
101
- nextSuggestion: config?.bindings?.nextSuggestion ?? globalConfig.bindings.nextSuggestion,
102
- previousSuggestion: config?.bindings?.previousSuggestion ?? globalConfig.bindings.previousSuggestion,
103
- acceptSuggestion: config?.bindings?.acceptSuggestion ?? globalConfig.bindings.acceptSuggestion,
104
- dismissSuggestions: config?.bindings?.dismissSuggestions ?? globalConfig.bindings.dismissSuggestions,
105
- },
106
- prompt: {
107
- bash: config.prompt?.bash,
108
- powershell: config.prompt?.powershell,
109
- xonsh: config.prompt?.xonsh,
110
- pwsh: config.prompt?.pwsh,
111
- nu: config.prompt?.nu,
112
- },
113
- specs: {
114
- path: [`${os.homedir()}/.fig/autocomplete/build`, ...(config?.specs?.path ?? [])],
115
- },
116
- };
117
- }
125
+ });
126
+ globalConfig.specs = { path: [`${os.homedir()}/.fig/autocomplete/build`, ...(globalConfig.specs?.path ?? [])] };
118
127
  };
119
128
  export const deleteCacheFolder = async () => {
120
129
  const cliConfigPath = path.join(os.homedir(), cachePath);
@@ -30,6 +30,7 @@ export const supportedShells = [
30
30
  Shell.Nushell,
31
31
  ].filter((shell) => shell != null);
32
32
  export const initSupportedShells = supportedShells.filter((shell) => shell != Shell.Cmd);
33
+ export const aliasSupportedShells = [Shell.Bash, Shell.Zsh];
33
34
  export const userZdotdir = process.env?.ZDOTDIR ?? os.homedir() ?? `~`;
34
35
  export const zdotdir = path.join(os.tmpdir(), `is-zsh`);
35
36
  const configFolder = ".inshellisense";
@@ -49,6 +50,23 @@ export const setupZshDotfiles = async () => {
49
50
  await fsAsync.cp(path.join(shellFolderPath, "shellIntegration-login.zsh"), path.join(zdotdir, ".zlogin"));
50
51
  };
51
52
  export const inferShell = async () => {
53
+ // try getting shell from shell specific env variables
54
+ if (process.env.NU_VERSION != null) {
55
+ return Shell.Nushell;
56
+ }
57
+ else if (process.env.XONSHRC != null) {
58
+ return Shell.Xonsh;
59
+ }
60
+ else if (process.env.FISH_VERSION != null) {
61
+ return Shell.Fish;
62
+ }
63
+ else if (process.env.ZSH_VERSION != null) {
64
+ return Shell.Zsh;
65
+ }
66
+ else if (process.env.BASH_VERSION != null) {
67
+ return Shell.Bash;
68
+ }
69
+ // try getting shell from env
52
70
  try {
53
71
  const name = path.parse(process.env.SHELL ?? "").name;
54
72
  const shellName = supportedShells.find((shell) => name.includes(shell));
@@ -58,6 +76,7 @@ export const inferShell = async () => {
58
76
  catch {
59
77
  /* empty */
60
78
  }
79
+ // try getting shell from parent process
61
80
  const processResult = (await find("pid", process.ppid)).at(0);
62
81
  const name = processResult?.name;
63
82
  return name != null ? supportedShells.find((shell) => name.includes(shell)) : undefined;
@@ -103,9 +122,22 @@ export const getShellPromptRewrites = (shell) => shell == Shell.Nushell;
103
122
  export const getShellConfig = (shell) => {
104
123
  switch (shell) {
105
124
  case Shell.Zsh:
125
+ return `if [[ -z "\${ISTERM}" && $- = *i* && $- != *c* ]]; then
126
+ if [[ -o login ]]; then
127
+ is -s zsh --login ; exit
128
+ else
129
+ is -s zsh ; exit
130
+ fi
131
+ fi`;
106
132
  case Shell.Bash:
107
133
  return `if [[ -z "\${ISTERM}" && $- = *i* && $- != *c* ]]; then
108
- is -s ${shell} ; exit
134
+ shopt -q login_shell
135
+ login_shell=$?
136
+ if [ $login_shell -eq 0 ]; then
137
+ is -s bash --login ; exit
138
+ else
139
+ is -s bash ; exit
140
+ fi
109
141
  fi`;
110
142
  case Shell.Powershell:
111
143
  case Shell.Pwsh:
@@ -118,14 +150,21 @@ if ([string]::IsNullOrEmpty($env:ISTERM) -and [Environment]::UserInteractive -an
118
150
  }`;
119
151
  case Shell.Fish:
120
152
  return `if test -z "$ISTERM" && status --is-interactive
121
- is -s fish ; kill %self
153
+ if status --is-login
154
+ is -s fish --login ; kill %self
155
+ else
156
+ is -s fish ; kill %self
157
+ end
122
158
  end`;
123
159
  case Shell.Xonsh:
124
160
  return `if 'ISTERM' not in \${...} and $XONSH_INTERACTIVE:
125
- is -s xonsh ; exit`;
161
+ if $XONSH_LOGIN:
162
+ is -s xonsh --login ; exit
163
+ else:
164
+ is -s xonsh ; exit`;
126
165
  case Shell.Nushell:
127
166
  return `if "ISTERM" not-in $env and $nu.is-interactive {
128
- is -s nu ; exit
167
+ if $nu.is-login { is -s nu --login ; exit } else { is -s nu ; exit }
129
168
  }`;
130
169
  }
131
170
  return "";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@microsoft/inshellisense",
3
- "version": "0.0.1-rc.13",
3
+ "version": "0.0.1-rc.15",
4
4
  "description": "IDE style command line auto complete",
5
5
  "type": "module",
6
6
  "engines": {
@@ -41,6 +41,7 @@
41
41
  "dependencies": {
42
42
  "@homebridge/node-pty-prebuilt-multiarch": "^0.11.12",
43
43
  "@withfig/autocomplete": "2.651.0",
44
+ "@xterm/headless": "^5.5.0",
44
45
  "ajv": "^8.12.0",
45
46
  "ansi-escapes": "^6.2.0",
46
47
  "ansi-styles": "^6.2.1",
@@ -48,11 +49,11 @@
48
49
  "color-convert": "^2.0.1",
49
50
  "commander": "^11.0.0",
50
51
  "find-process": "^1.4.7",
52
+ "strip-ansi": "^7.1.0",
51
53
  "toml": "^3.0.0",
52
54
  "wcwidth": "^1.0.1",
53
55
  "which": "^4.0.0",
54
- "wrap-ansi": "^8.1.0",
55
- "@xterm/headless": "^5.3.0"
56
+ "wrap-ansi": "^8.1.0"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@microsoft/tui-test": "^0.0.1-rc.3",
@@ -65,6 +66,7 @@
65
66
  "@typescript-eslint/eslint-plugin": "^6.7.4",
66
67
  "@typescript-eslint/parser": "^6.7.4",
67
68
  "@withfig/autocomplete-types": "^1.28.0",
69
+ "@xterm/xterm": "^5.5.0",
68
70
  "eslint": "^8.51.0",
69
71
  "eslint-config-prettier": "^9.0.0",
70
72
  "eslint-plugin-header": "^3.1.1",
@@ -1,5 +1,19 @@
1
- if [ -r ~/.bashrc ]; then
2
- . ~/.bashrc
1
+ if [ -z "$ISTERM_LOGIN" ]; then
2
+ if [ -r ~/.bashrc ]; then
3
+ . ~/.bashrc
4
+ fi
5
+ else
6
+ if [ -r /etc/profile ]; then
7
+ . /etc/profile
8
+ fi
9
+ # execute the first that exists
10
+ if [ -r ~/.bash_profile ]; then
11
+ . ~/.bash_profile
12
+ elif [ -r ~/.bash_login ]; then
13
+ . ~/.bash_login
14
+ elif [ -r ~/.profile ]; then
15
+ . ~/.profile
16
+ fi
3
17
  fi
4
18
 
5
19
  if [ -r ~/.inshellisense/bash-preexec.sh ]; then
@@ -26,6 +40,10 @@ __is_escape_value() {
26
40
  token="\\\\"
27
41
  elif [ "$byte" = ";" ]; then
28
42
  token="\\x3b"
43
+ elif [ "$byte" = $'\n' ]; then
44
+ token="\x0a"
45
+ elif [ "$byte" = $'\e' ]; then
46
+ token="\\x1b"
29
47
  else
30
48
  token="$byte"
31
49
  fi
@@ -40,6 +58,16 @@ __is_update_cwd() {
40
58
  builtin printf '\e]6973;CWD;%s\a' "$(__is_escape_value "$PWD")"
41
59
  }
42
60
 
61
+ __is_report_prompt() {
62
+ if ((BASH_VERSINFO[0] >= 4)); then
63
+ __is_prompt=${__is_original_PS1@P}
64
+ else
65
+ __is_prompt=${__is_original_PS1}
66
+ fi
67
+ __is_prompt="$(builtin printf "%s" "${__is_prompt//[$'\001'$'\002']}")"
68
+ builtin printf "\e]6973;PROMPT;%s\a" "$(__is_escape_value "${__is_prompt}")"
69
+ }
70
+
43
71
  if [[ -n "${bash_preexec_imported:-}" ]]; then
44
72
  precmd_functions+=(__is_precmd)
45
73
  fi
@@ -47,6 +75,7 @@ fi
47
75
  __is_precmd() {
48
76
  __is_update_cwd
49
77
  __is_update_prompt
78
+ __is_report_prompt
50
79
  }
51
80
 
52
81
  __is_update_prompt() {
@@ -2,15 +2,18 @@ function __is_copy_function; functions $argv[1] | sed "s/^function $argv[1]/func
2
2
  function __is_prompt_start; printf '\e]6973;PS\a'; end
3
3
  function __is_prompt_end; printf '\e]6973;PE\a'; end
4
4
 
5
+ __is_copy_function fish_prompt is_user_prompt
6
+
5
7
  function __is_escape_value
6
8
  echo $argv \
7
- | string replace --all '\\' '\\\\' \
8
- | string replace --all ';' '\\x3b' \
9
+ | string replace -a '\\' '\\\\' \
10
+ | string replace -a ';' '\\x3b' \
11
+ | string replace -a \e '\\x1b' \
12
+ | string split \n | string join '\x0a' \
9
13
  ;
10
14
  end
11
- function __is_update_cwd --on-event fish_prompt; set __is_cwd (__is_escape_value "$PWD"); printf "\e]6973;CWD;$__is_cwd\a"; end
12
-
13
- __is_copy_function fish_prompt is_user_prompt
15
+ function __is_update_cwd --on-event fish_prompt; set __is_cwd (__is_escape_value "$PWD"); printf "\e]6973;CWD;%s\a" $__is_cwd; end
16
+ function __is_report_prompt --on-event fish_prompt; set __is_prompt (__is_escape_value (is_user_prompt)); printf "\e]6973;PROMPT;%s\a" $__is_prompt; end
14
17
 
15
18
  if [ "$ISTERM_TESTING" = "1" ]
16
19
  function is_user_prompt; printf '> '; end
@@ -1,18 +1,26 @@
1
- let __is_escape_value = {|x| $x | str replace --all "\\" "\\\\" | str replace --all ";" "\\x3b" }
1
+ let __is_escape_value = {|x| $x | str replace --all "\\" "\\\\" | str replace --all ";" "\\x3b" | str replace --all "\n" '\x0a' | str replace --all "\e" "\\x1b" }
2
+ let __is_original_PROMPT_COMMAND = if 'PROMPT_COMMAND' in $env { $env.PROMPT_COMMAND } else { "" }
3
+ let __is_original_PROMPT_INDICATOR = if 'PROMPT_INDICATOR' in $env { $env.PROMPT_INDICATOR } else { "" }
2
4
 
3
5
  let __is_update_cwd = {
4
6
  let pwd = do $__is_escape_value $env.PWD
5
7
  $"\e]6973;CWD;($pwd)\a"
6
8
  }
7
- let __is_original_PROMPT_COMMAND = if 'PROMPT_COMMAND' in $env { $env.PROMPT_COMMAND } else { "" }
9
+ let __is_report_prompt = {
10
+ let __is_indicatorCommandType = $__is_original_PROMPT_INDICATOR | describe
11
+ mut __is_prompt_ind = if $__is_indicatorCommandType == "closure" { do $__is_original_PROMPT_INDICATOR } else { $__is_original_PROMPT_INDICATOR }
12
+ let __is_esc_prompt_ind = do $__is_escape_value $__is_prompt_ind
13
+ $"\e]6973;PROMPT;($__is_esc_prompt_ind)\a"
14
+ }
8
15
  let __is_custom_PROMPT_COMMAND = {
9
16
  let promptCommandType = $__is_original_PROMPT_COMMAND | describe
10
17
  mut cmd = if $promptCommandType == "closure" { do $__is_original_PROMPT_COMMAND } else { $__is_original_PROMPT_COMMAND }
11
18
  let pwd = do $__is_update_cwd
19
+ let prompt = do $__is_report_prompt
12
20
  if 'ISTERM_TESTING' in $env {
13
21
  $cmd = ""
14
22
  }
15
- $"\e]6973;PS\a($cmd)($pwd)"
23
+ $"\e]6973;PS\a($cmd)($pwd)($prompt)"
16
24
  }
17
25
  $env.PROMPT_COMMAND = $__is_custom_PROMPT_COMMAND
18
26
 
@@ -8,7 +8,7 @@ if ($env:ISTERM_TESTING -eq "1") {
8
8
  }
9
9
 
10
10
  function Global:__IS-Escape-Value([string]$value) {
11
- [regex]::Replace($value, '[\\\n;]', { param($match)
11
+ [regex]::Replace($value, "[$([char]0x1b)\\\n;]", { param($match)
12
12
  -Join (
13
13
  [System.Text.Encoding]::UTF8.GetBytes($match.Value) | ForEach-Object { '\x{0:x2}' -f $_ }
14
14
  )
@@ -17,8 +17,11 @@ function Global:__IS-Escape-Value([string]$value) {
17
17
 
18
18
  function Global:Prompt() {
19
19
  $Result = "$([char]0x1b)]6973;PS`a"
20
- $Result += $Global:__IsOriginalPrompt.Invoke()
20
+ $OriginalPrompt += $Global:__IsOriginalPrompt.Invoke()
21
+ $Result += $OriginalPrompt
21
22
  $Result += "$([char]0x1b)]6973;PE`a"
23
+
24
+ $Result += "$([char]0x1b)]6973;PROMPT;$(__IS-Escape-Value $OriginalPrompt)`a"
22
25
  $Result += if ($pwd.Provider.Name -eq 'FileSystem') { "$([char]0x1b)]6973;CWD;$(__IS-Escape-Value $pwd.ProviderPath)`a" }
23
26
  return $Result
24
27
  }
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from xonsh.main import XSH
2
3
 
3
4
  def __is_prompt_start() -> str:
4
5
  return "\001" + "\x1b]6973;PS\x07"
@@ -7,23 +8,30 @@ def __is_prompt_start() -> str:
7
8
  def __is_prompt_end() -> str:
8
9
  return "\001" + "\x1b]6973;PE\x07" + "\002"
9
10
 
10
-
11
11
  def __is_escape_value(value: str) -> str:
12
12
  byte_list = [bytes([byte]).decode("utf-8") for byte in list(value.encode("utf-8"))]
13
13
  return "".join(
14
14
  [
15
- "\\x3b" if byte == ";" else "\\\\" if byte == "\\" else byte
15
+ "\\x3b" if byte == ";" else "\\\\" if byte == "\\" else "\\x1b" if byte == "\x1b" else "\x0a" if byte == "\n" else byte
16
16
  for byte in byte_list
17
17
  ]
18
18
  )
19
19
 
20
20
  def __is_update_cwd() -> str:
21
- return f"\x1b]6973;CWD;{__is_escape_value(os.getcwd())}\x07" + "\002"
21
+ return f"\x1b]6973;CWD;{__is_escape_value(os.getcwd())}\x07"
22
+
23
+ __is_original_prompt = $PROMPT
24
+ def __is_report_prompt() -> str:
25
+ prompt = ""
26
+ formatted_prompt = XSH.shell.prompt_formatter(__is_original_prompt)
27
+ prompt = "".join([text for _, text in XSH.shell.format_color(formatted_prompt)])
28
+ return f"\x1b]6973;PROMPT;{__is_escape_value(prompt)}\x07" + "\002"
22
29
 
23
30
  $PROMPT_FIELDS['__is_prompt_start'] = __is_prompt_start
24
31
  $PROMPT_FIELDS['__is_prompt_end'] = __is_prompt_end
25
32
  $PROMPT_FIELDS['__is_update_cwd'] = __is_update_cwd
33
+ $PROMPT_FIELDS['__is_report_prompt'] = __is_report_prompt
26
34
  if 'ISTERM_TESTING' in ${...}:
27
35
  $PROMPT = "> "
28
36
 
29
- $PROMPT = "{__is_prompt_start}{__is_update_cwd}" + $PROMPT + "{__is_prompt_end}"
37
+ $PROMPT = "{__is_prompt_start}{__is_update_cwd}{__is_report_prompt}" + $PROMPT + "{__is_prompt_end}"