diff --git a/.devcontainer.json b/.devcontainer.json
index 4d87578..59fd391 100644
--- a/.devcontainer.json
+++ b/.devcontainer.json
@@ -1,3 +1,3 @@
{
- "image": "mcr.microsoft.com/devcontainers/javascript-node"
+ "image": "devpit:latest"
}
\ No newline at end of file
diff --git a/src/terminalService.js b/src/terminalService.js
index bd53076..e33fa84 100644
--- a/src/terminalService.js
+++ b/src/terminalService.js
@@ -35,19 +35,24 @@ class TerminalService extends EventEmitter {
});
this.ptyProcess.onData((data) => {
- // Normalize line endings
- const normalized = String(data).replace(/\r\n/g, '\n').replace(/\r/g, '\n');
- const parts = normalized.split('\n');
-
- // First part joins the existing partial
- if (parts.length > 0) {
- this.partial += parts[0];
- }
-
- // For each subsequent part before the last, we have completed lines
- for (let i = 1; i < parts.length; i += 1) {
- this.lines.push(this.partial);
- this.partial = parts[i];
+ const str = String(data);
+ for (let i = 0; i < str.length; i += 1) {
+ const ch = str[i];
+ if (ch === '\\n') {
+ // Line feed completes the current line
+ this.lines.push(this.partial);
+ this.partial = '';
+ } else if (ch === '\\r') {
+ // Carriage return: move to start of line; start overwriting
+ this.partial = '';
+ } else if (ch === '\\b' || ch === '\\x7f') {
+ // Backspace or DEL: remove last char if present
+ if (this.partial.length > 0) {
+ this.partial = this.partial.slice(0, -1);
+ }
+ } else {
+ this.partial += ch;
+ }
}
// Enforce max lines buffer
diff --git a/src/ui/InkApp.jsx b/src/ui/InkApp.jsx
index 44c4c24..e7664ed 100644
--- a/src/ui/InkApp.jsx
+++ b/src/ui/InkApp.jsx
@@ -4,6 +4,26 @@ import TextInput from 'ink-text-input';
import terminalService from '../terminalService.js';
class Pane extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ cursorVisible: true,
+ };
+ this._cursorTimer = null;
+ }
+ componentDidMount() {
+ if (this.props.showCursor) {
+ this._cursorTimer = setInterval(() => {
+ this.setState((s) => ({ cursorVisible: !s.cursorVisible }));
+ }, typeof this.props.cursorBlinkMs === 'number' && this.props.cursorBlinkMs > 0 ? this.props.cursorBlinkMs : 500);
+ }
+ }
+ componentWillUnmount() {
+ if (this._cursorTimer) {
+ clearInterval(this._cursorTimer);
+ this._cursorTimer = null;
+ }
+ }
// Strip ANSI escape sequences so width measurement/truncation is accurate
stripAnsi(input) {
if (input == null) return '';
@@ -35,6 +55,26 @@ class Pane extends React.Component {
}
return out;
}
+ // Apply a blinking cursor to the given line according to width constraints
+ withCursor(line, maxWidth) {
+ const cursorChar = typeof this.props.cursorChar === 'string' && this.props.cursorChar.length > 0 ? this.props.cursorChar[0] : '█';
+ const visible = !!this.state.cursorVisible;
+ if (!visible) {
+ return line;
+ }
+ if (typeof maxWidth === 'number' && maxWidth > 0) {
+ if (line.length < maxWidth) {
+ return `${line}${cursorChar}`;
+ }
+ if (line.length === maxWidth) {
+ // Replace last char to avoid overflow
+ return `${line.slice(0, maxWidth - 1)}${cursorChar}`;
+ }
+ // If somehow longer, just ensure width
+ return `${line.slice(0, maxWidth - 1)}${cursorChar}`;
+ }
+ return `${line}${cursorChar}`;
+ }
render() {
const { title, lines, maxWidth } = this.props;
return (
@@ -42,20 +82,17 @@ class Pane extends React.Component {
{title}
{(lines && lines.length > 0)
- ? lines.map((line, index) => (
- {
- (() => {
- const clean = this.stripAnsi(line);
- const width = typeof maxWidth === 'number' && maxWidth > 0 ? maxWidth : undefined;
- // Expand tabs before slicing to visual width
- const expanded = this.expandTabs(clean, 8, width);
- if (width && expanded.length > width) {
- return expanded.slice(0, width);
- }
- return expanded;
- })()
- }
- ))
+ ? lines.map((line, index) => {
+ const isLast = index === lines.length - 1;
+ const width = typeof maxWidth === 'number' && maxWidth > 0 ? maxWidth : undefined;
+ const clean = this.stripAnsi(line);
+ const expanded = this.expandTabs(clean, 8, width);
+ const baseLine = (width && expanded.length > width) ? expanded.slice(0, width) : expanded;
+ const finalLine = (this.props.showCursor && isLast) ? this.withCursor(baseLine, width) : baseLine;
+ return (
+ {finalLine}
+ );
+ })
: —
}
@@ -201,7 +238,7 @@ export default class InkApp extends React.Component {
-
+