diff --git a/examples/ted/Properties/launchSettings.json b/examples/ted/Properties/launchSettings.json
new file mode 100644
index 0000000..5d9d44a
--- /dev/null
+++ b/examples/ted/Properties/launchSettings.json
@@ -0,0 +1,9 @@
+{
+ "profiles": {
+ "ted": {
+ "commandName": "Project",
+ "commandLineArgs": "examples/ted/TedApp.cs",
+ "workingDirectory": "../.."
+ }
+ }
+}
\ No newline at end of file
diff --git a/examples/ted/TedApp.cs b/examples/ted/TedApp.cs
index 1c6e97a..5f1d109 100644
--- a/examples/ted/TedApp.cs
+++ b/examples/ted/TedApp.cs
@@ -139,6 +139,8 @@ public TedApp (bool readOnly = false)
new ([
new Shortcut { Title = "Language", CommandView = LanguageShortcut },
new Shortcut { Title = "Theme", CommandView = ThemeDropDown },
+ OverwriteShortcut = new Shortcut (Key.Empty, "INS", null)
+ { MouseHighlightStates = MouseState.None },
LocShortcut = new Shortcut (Key.Empty, FormatLoc (1, 1), null)
{ MouseHighlightStates = MouseState.None }
])
@@ -264,6 +266,7 @@ [new MenuItem ("_About", "About ted", ShowAboutDialog)]),
// Editor.CaretChanged covers both user-driven movement and document edits that shift the
// caret (insert/remove). Initial render seeds the value before any movement happens.
Editor.CaretChanged += (_, _) => UpdateLocShortcut ();
+ Editor.OverwriteModeChanged += (_, _) => UpdateOverwriteShortcut ();
Editor.FindRequested += (_, _) => ShowFindReplaceDialog (false);
Editor.ReplaceRequested += (_, _) => ShowFindReplaceDialog (true);
UpdateLocShortcut ();
@@ -293,6 +296,12 @@ [new MenuItem ("_About", "About ted", ShowAboutDialog)]),
///
public Shortcut LocShortcut { get; }
+ ///
+ /// The status-bar shortcut that shows whether the editor is in insert (INS) or overwrite (OVR)
+ /// mode. Updated whenever fires.
+ ///
+ public Shortcut OverwriteShortcut { get; }
+
///
/// Resolves the key shortcut for by asking the 's
/// first; falls back to for
@@ -382,6 +391,12 @@ private void UpdateLocShortcut ()
LocShortcut.SetNeedsDraw ();
}
+ private void UpdateOverwriteShortcut ()
+ {
+ OverwriteShortcut.Title = Editor.OverwriteMode ? "OVR" : "INS";
+ OverwriteShortcut.SetNeedsDraw ();
+ }
+
private static string FormatLoc (int line, int column)
{
return $"Ln {line}, Col {column}";
diff --git a/specs/overwrite-mode/spec.md b/specs/overwrite-mode/spec.md
new file mode 100644
index 0000000..8e8465b
--- /dev/null
+++ b/specs/overwrite-mode/spec.md
@@ -0,0 +1,67 @@
+# Overwrite (Insert-Replace) Mode
+
+**Status**: Implemented
+**Issue**: [#146](https://github.com/gui-cs/Editor/issues/146)
+**Updated**: 2026-05-17
+
+## Summary
+
+`Editor` supports an overwrite mode: when active, typed characters replace the grapheme under
+the caret instead of inserting before it. At line-end or when a selection is active, typing
+still inserts. The mode is toggled via the Insert key and can be controlled programmatically.
+
+## Public API
+
+```csharp
+public partial class Editor : View
+{
+ /// Gets or sets whether the editor is in overwrite mode.
+ public bool OverwriteMode { get; set; }
+
+ /// Raised whenever OverwriteMode changes.
+ public event EventHandler? OverwriteModeChanged;
+}
+```
+
+## Commands & Key Bindings
+
+| Command | Default Key | Behaviour |
+|----------------------------|-------------|------------------------------|
+| `Command.ToggleOverwrite` | Insert | Toggles `OverwriteMode` |
+| `Command.EnableOverwrite` | *(none)* | Sets `OverwriteMode = true` |
+| `Command.DisableOverwrite` | *(none)* | Sets `OverwriteMode = false` |
+
+All three are wired through `AddCommand` and the `ToggleOverwrite` binding lives in
+`Editor.DefaultKeyBindings` (user-overridable via `[ConfigurationProperty]`).
+
+## Typing Behaviour
+
+- **Overwrite on, no selection, caret not at line-end**: the grapheme cluster at the caret
+ is replaced by the typed character. Uses `RemoveAndInsert` offset mapping so the caret
+ anchor advances past the inserted text. Wide-rune safe (uses
+ `StringInfo.GetNextTextElementLength`).
+- **Overwrite on, selection active**: selection is replaced (same as insert mode).
+- **Overwrite on, caret at line-end**: plain insert (newline is never consumed).
+- **Multi-caret**: each additional caret follows the same overwrite logic.
+- **Undo**: each overwrite is a single undo step.
+
+## Caret Rendering
+
+While `OverwriteMode` is active, the cursor style is forced to `CursorStyle.SteadyBlock`
+(solid block), distinct from the default bar/underline style used in insert mode.
+
+## ted Integration
+
+The `ted` demo shows an **INS** / **OVR** indicator in the status bar, updated whenever
+`OverwriteModeChanged` fires.
+
+## Files Changed
+
+- `src/Terminal.Gui.Editor/Editor.cs` — `OverwriteMode` property + `OverwriteModeChanged` event
+- `src/Terminal.Gui.Editor/Editor.Commands.cs` — commands, key binding, `OverwriteAtOffset` helper
+- `src/Terminal.Gui.Editor/Editor.Keyboard.cs` — overwrite path in `OnKeyDownNotHandled`
+- `src/Terminal.Gui.Editor/Editor.Drawing.cs` — `SteadyBlock` cursor in overwrite mode
+- `src/Terminal.Gui.Editor/Editor.MultiCaret.cs` — overwrite in multi-caret insert
+- `examples/ted/TedApp.cs` — INS/OVR status bar indicator
+- `specs/public-api.md` — updated with new property and event
+- `tests/Terminal.Gui.Editor.IntegrationTests/EditorOverwriteTests.cs` — integration tests
diff --git a/specs/public-api.md b/specs/public-api.md
index bca4cb7..f54ba07 100644
--- a/specs/public-api.md
+++ b/specs/public-api.md
@@ -36,6 +36,8 @@ public class Editor : View
public bool ShowLineNumbers { get; set; } // exists
public bool WordWrap { get; set; } // word-wrap-toggle (needs word-wrap)
public bool ReadOnly { get; set; } // exists (read-only ✅)
+ public bool OverwriteMode { get; set; } // exists (overwrite-mode ✅)
+ public event EventHandler? OverwriteModeChanged; // exists (overwrite-mode ✅)
// --- Indentation (tab-handling ✅ + auto-indent) ---
public int IndentationSize { get; set; } = 4; // exists (codex merge)
diff --git a/src/Terminal.Gui.Editor/Editor.Commands.cs b/src/Terminal.Gui.Editor/Editor.Commands.cs
index 732a34f..4b5e5f3 100644
--- a/src/Terminal.Gui.Editor/Editor.Commands.cs
+++ b/src/Terminal.Gui.Editor/Editor.Commands.cs
@@ -52,7 +52,8 @@ public partial class Editor
[Command.WordLeftExtend] = Bind.All (Key.CursorLeft.WithCtrl.WithShift),
[Command.WordRightExtend] = Bind.All (Key.CursorRight.WithCtrl.WithShift),
[Command.KillWordLeft] = Bind.All (Key.Backspace.WithCtrl),
- [Command.KillWordRight] = Bind.All (Key.Delete.WithCtrl)
+ [Command.KillWordRight] = Bind.All (Key.Delete.WithCtrl),
+ [Command.ToggleOverwrite] = Bind.All (Key.InsertChar)
};
private void CreateCommandsAndBindings ()
@@ -244,6 +245,26 @@ private void CreateCommandsAndBindings ()
return true;
});
+ // Overwrite mode
+ AddCommand (Command.ToggleOverwrite, () =>
+ {
+ OverwriteMode = !OverwriteMode;
+
+ return true;
+ });
+ AddCommand (Command.EnableOverwrite, () =>
+ {
+ OverwriteMode = true;
+
+ return true;
+ });
+ AddCommand (Command.DisableOverwrite, () =>
+ {
+ OverwriteMode = false;
+
+ return true;
+ });
+
ApplyKeyBindings (View.DefaultKeyBindings, DefaultKeyBindings);
// Reclaim Tab / Shift+Tab from the framework's default focus-cycling bindings so our
@@ -350,6 +371,10 @@ private void CreateCommandsAndBindings ()
{
ReplaceSelection (text);
}
+ else if (OverwriteMode && _document is not null)
+ {
+ OverwriteAtCaret (text);
+ }
else
{
_document!.Insert (CaretOffset, text);
@@ -358,6 +383,43 @@ private void CreateCommandsAndBindings ()
return true;
}
+ ///
+ /// Overwrites the grapheme at the caret with . If the caret is at
+ /// line-end, falls back to a plain insert so the newline is not consumed.
+ ///
+ private void OverwriteAtCaret (string text)
+ {
+ OverwriteAtOffset (CaretOffset, text);
+ }
+
+ ///
+ /// Overwrites the grapheme at the given with .
+ /// If the offset is at line-end, falls back to a plain insert so the newline is not consumed.
+ ///
+ private void OverwriteAtOffset (int offset, string text)
+ {
+ DocumentLine line = _document!.GetLineByOffset (offset);
+ var lineEnd = line.Offset + line.Length;
+
+ if (offset >= lineEnd)
+ {
+ // At or past end-of-line content — just insert.
+ _document.Insert (offset, text);
+
+ return;
+ }
+
+ // Determine the length of the grapheme cluster under the caret so wide runes are
+ // replaced atomically. StringInfo.GetNextTextElementLength gives cluster length in chars.
+ var remaining = _document.GetText (offset, lineEnd - offset);
+ var graphemeLength = System.Globalization.StringInfo.GetNextTextElementLength (remaining);
+
+ // Use RemoveAndInsert so that the caret anchor (AfterInsertion) moves past the
+ // inserted text. The default same-length Replace uses CharacterReplace mode which
+ // does not move anchors at all.
+ _document.Replace (offset, graphemeLength, text, OffsetChangeMappingType.RemoveAndInsert);
+ }
+
private bool? DeleteLeft ()
{
if (ReadOnly)
diff --git a/src/Terminal.Gui.Editor/Editor.Drawing.cs b/src/Terminal.Gui.Editor/Editor.Drawing.cs
index caa7172..23018c6 100644
--- a/src/Terminal.Gui.Editor/Editor.Drawing.cs
+++ b/src/Terminal.Gui.Editor/Editor.Drawing.cs
@@ -362,10 +362,12 @@ private void UpdateCursor ()
}
Point screen = ViewportToScreen (new Point (col, row));
+ CursorStyle style = OverwriteMode ? CursorStyle.SteadyBlock :
+ Cursor.Style == CursorStyle.Hidden ? CursorStyle.Default : Cursor.Style;
Cursor = Cursor with
{
Position = screen,
- Style = Cursor.Style == CursorStyle.Hidden ? CursorStyle.Default : Cursor.Style
+ Style = style
};
}
}
diff --git a/src/Terminal.Gui.Editor/Editor.Keyboard.cs b/src/Terminal.Gui.Editor/Editor.Keyboard.cs
index 7f48404..7d46217 100644
--- a/src/Terminal.Gui.Editor/Editor.Keyboard.cs
+++ b/src/Terminal.Gui.Editor/Editor.Keyboard.cs
@@ -47,6 +47,10 @@ protected override bool OnKeyDownNotHandled (Key key)
{
ReplaceSelection (rune.ToString ());
}
+ else if (OverwriteMode && _document is not null)
+ {
+ OverwriteAtCaret (rune.ToString ());
+ }
else
{
_document!.Insert (CaretOffset, rune.ToString ());
diff --git a/src/Terminal.Gui.Editor/Editor.MultiCaret.cs b/src/Terminal.Gui.Editor/Editor.MultiCaret.cs
index b7534a4..f1df656 100644
--- a/src/Terminal.Gui.Editor/Editor.MultiCaret.cs
+++ b/src/Terminal.Gui.Editor/Editor.MultiCaret.cs
@@ -309,6 +309,10 @@ private void MultiCaretInsert (string text)
{
ReplaceSelection (text);
}
+ else if (OverwriteMode)
+ {
+ OverwriteAtCaret (text);
+ }
else
{
_document.Insert (CaretOffset, text);
@@ -329,7 +333,14 @@ private void MultiCaretInsert (string text)
}
}
- _document.Insert (caret.Offset, text);
+ if (OverwriteMode)
+ {
+ OverwriteAtOffset (caret.Offset, text);
+ }
+ else
+ {
+ _document.Insert (caret.Offset, text);
+ }
}
}
}
diff --git a/src/Terminal.Gui.Editor/Editor.cs b/src/Terminal.Gui.Editor/Editor.cs
index de8ad05..fd3df13 100644
--- a/src/Terminal.Gui.Editor/Editor.cs
+++ b/src/Terminal.Gui.Editor/Editor.cs
@@ -362,6 +362,30 @@ private void EnsureCaretNotInFold ()
}
}
+ ///
+ /// Gets or sets whether the editor is in overwrite mode. When , typed
+ /// characters replace the grapheme under the caret instead of inserting before it. At line-end
+ /// or when a selection is active, the insertion still inserts. Defaults to .
+ ///
+ public bool OverwriteMode
+ {
+ get;
+ set
+ {
+ if (field == value)
+ {
+ return;
+ }
+
+ field = value;
+ SetNeedsDraw ();
+ OverwriteModeChanged?.Invoke (this, EventArgs.Empty);
+ }
+ }
+
+ /// Raised whenever changes.
+ public event EventHandler? OverwriteModeChanged;
+
/// Raised whenever changes.
public event EventHandler? CaretChanged;
diff --git a/tests/Terminal.Gui.Editor.IntegrationTests/EditorOverwriteTests.cs b/tests/Terminal.Gui.Editor.IntegrationTests/EditorOverwriteTests.cs
new file mode 100644
index 0000000..9cd7dc3
--- /dev/null
+++ b/tests/Terminal.Gui.Editor.IntegrationTests/EditorOverwriteTests.cs
@@ -0,0 +1,162 @@
+// Copilot - gpt-4.1
+
+using Terminal.Gui.Editor.IntegrationTests.Testing;
+using Terminal.Gui.Input;
+using Terminal.Gui.Testing;
+using Xunit;
+
+namespace Terminal.Gui.Editor.IntegrationTests;
+
+///
+/// Integration tests for overwrite (insert-replace) mode in .
+///
+public class EditorOverwriteTests
+{
+ private static readonly InputInjectionOptions Direct = new () { Mode = InputInjectionMode.Direct };
+
+ // ───────────────────── Property / Command ─────────────────────
+
+ [Fact]
+ public void OverwriteMode_DefaultsToFalse ()
+ {
+ Editor editor = new ();
+ Assert.False (editor.OverwriteMode);
+ }
+
+ [Fact]
+ public void OverwriteMode_RaisesEvent ()
+ {
+ Editor editor = new ();
+ var raised = false;
+ editor.OverwriteModeChanged += (_, _) => raised = true;
+ editor.OverwriteMode = true;
+ Assert.True (raised);
+ }
+
+ [Fact]
+ public void DefaultKeyBindings_Contains_ToggleOverwrite ()
+ {
+ Assert.True (Editor.DefaultKeyBindings!.ContainsKey (Command.ToggleOverwrite));
+ }
+
+ [Fact]
+ public async Task InsertKey_Toggles_OverwriteMode ()
+ {
+ await using AppFixture fx = new (() => new ("abc"));
+ fx.Top.Editor.SetFocus ();
+
+ Assert.False (fx.Top.Editor.OverwriteMode);
+
+ fx.Injector.InjectKey (Key.InsertChar, Direct);
+ Assert.True (fx.Top.Editor.OverwriteMode);
+
+ fx.Injector.InjectKey (Key.InsertChar, Direct);
+ Assert.False (fx.Top.Editor.OverwriteMode);
+ }
+
+ // ───────────────────── Overwrite typing behaviour ─────────────────────
+
+ [Fact]
+ public async Task Overwrite_Replaces_CharacterAtCaret ()
+ {
+ await using AppFixture fx = new (() => new ("abc"));
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 0;
+ fx.Top.Editor.OverwriteMode = true;
+
+ fx.Injector.InjectKey (Key.X, Direct);
+
+ Assert.Equal ("xbc", fx.Top.Editor.Document?.Text);
+ Assert.Equal (1, fx.Top.Editor.CaretOffset);
+ }
+
+ [Fact]
+ public async Task Overwrite_AtLineEnd_Inserts ()
+ {
+ await using AppFixture fx = new (() => new ("ab"));
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 2; // at end of "ab"
+ fx.Top.Editor.OverwriteMode = true;
+
+ fx.Injector.InjectKey (Key.X, Direct);
+
+ Assert.Equal ("abx", fx.Top.Editor.Document?.Text);
+ Assert.Equal (3, fx.Top.Editor.CaretOffset);
+ }
+
+ [Fact]
+ public async Task Overwrite_WithSelection_ReplacesSelection ()
+ {
+ await using AppFixture fx = new (() => new ("abcdef"));
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.OverwriteMode = true;
+
+ // Select "bcd" (offsets 1..4) via Shift+Right
+ fx.Top.Editor.CaretOffset = 1;
+ fx.Injector.InjectKey (Key.CursorRight.WithShift, Direct);
+ fx.Injector.InjectKey (Key.CursorRight.WithShift, Direct);
+ fx.Injector.InjectKey (Key.CursorRight.WithShift, Direct);
+ Assert.True (fx.Top.Editor.HasSelection);
+
+ fx.Injector.InjectKey (Key.X, Direct);
+
+ // Selection should be replaced entirely, not overwrite-style.
+ Assert.Equal ("axef", fx.Top.Editor.Document?.Text);
+ }
+
+ [Fact]
+ public async Task Overwrite_SingleUndo_Step ()
+ {
+ await using AppFixture fx = new (() => new ("abc"));
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 0;
+ fx.Top.Editor.OverwriteMode = true;
+
+ fx.Injector.InjectKey (Key.X, Direct);
+ Assert.Equal ("xbc", fx.Top.Editor.Document?.Text);
+
+ fx.Top.Editor.Document!.UndoStack.Undo ();
+ Assert.Equal ("abc", fx.Top.Editor.Document.Text);
+ }
+
+ [Fact]
+ public async Task Overwrite_MultiLine_DoesNotConsumeNewline ()
+ {
+ await using AppFixture fx = new (() => new ("ab\ncd"));
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.CaretOffset = 1; // at 'b'
+ fx.Top.Editor.OverwriteMode = true;
+
+ // Overwrite 'b', then caret at offset 2 is at line-end (\n), should insert
+ fx.Injector.InjectKey (Key.X, Direct);
+ Assert.Equal ("ax\ncd", fx.Top.Editor.Document?.Text);
+ Assert.Equal (2, fx.Top.Editor.CaretOffset);
+
+ // Now at line-end — type inserts rather than consuming newline
+ fx.Injector.InjectKey (Key.Y, Direct);
+ Assert.Equal ("axy\ncd", fx.Top.Editor.Document?.Text);
+ }
+
+ // ───────────────────── Enable / Disable commands ─────────────────────
+
+ [Fact]
+ public async Task EnableOverwrite_Command_SetsMode ()
+ {
+ await using AppFixture fx = new (() => new ("abc"));
+ fx.Top.Editor.SetFocus ();
+
+ fx.Top.Editor.InvokeCommand (Command.EnableOverwrite);
+ Assert.True (fx.Top.Editor.OverwriteMode);
+ }
+
+ [Fact]
+ public async Task DisableOverwrite_Command_ClearsMode ()
+ {
+ await using AppFixture fx = new (() => new ("abc"));
+ fx.Top.Editor.SetFocus ();
+ fx.Top.Editor.OverwriteMode = true;
+
+ fx.Top.Editor.InvokeCommand (Command.DisableOverwrite);
+ Assert.False (fx.Top.Editor.OverwriteMode);
+ }
+}