Compare commits

..

No commits in common. "44356e5d3073e64c3c6e09ccf6465126bc9452e2" and "934cfcd9e2d4b9c24b2c4779752685d6265b10e7" have entirely different histories.

8 changed files with 94 additions and 171 deletions

View file

@ -8,14 +8,13 @@ and is my daily driver for almost everything.
# Features # Features
- **Lightning Fast** TUI with ≤6ms frame times, **low latency** input - **Lightning Fast** TUI with ≤6ms frame times, **low latency** input handling
handling and smooth **animated scrolling** and smooth **animated scrolling**
- Intuitive UI with **tabs**, **scrollbars** and **palettes** with full - Intuitive UI with **tabs**, **scrollbars** and **palettes** with full
**mouse** support for all UI elements **mouse** support for all UI elements
- Support for more than **70 programming languages**, **zero - Support for more than **70 programming languages**, **zero
configuration** needed, via **tree-sitter** powered syntax highlighting configuration** needed, via **tree-sitter** powered syntax highlighting
- **Language Server Protocol** pre configured support for most language - **Language Server Protocol** pre configured support for most language servers
servers
- Powerful **multi-cursor** editing and integrated **clipboard history** - Powerful **multi-cursor** editing and integrated **clipboard history**
- Powerful configurable keybinding system that supports **modal** and - Powerful configurable keybinding system that supports **modal** and
**non-modal** editing styles **non-modal** editing styles
@ -28,29 +27,26 @@ and is my daily driver for almost everything.
- Hybrid rope/piece-table buffer system, edit **very large files** with - Hybrid rope/piece-table buffer system, edit **very large files** with
**thousands of cursors** **thousands of cursors**
- Infinite **undo** (at least until you run out of ram) - Infinite **undo** (at least until you run out of ram)
- Full **unicode** support, including support for the kitty text sizing - Full **unicode** support, including support for the kitty text sizing protocol
protocol
- Plenty of **themes** included and support for vscode themes via the - Plenty of **themes** included and support for vscode themes via the
flow-themes project flow-themes project
- Runs on **Linux, FreeBSD, MacOS, Windows and Android** (under termux) - Runs on **Linux, FreeBSD, MacOS, Windows and Android** (under termux) with
with easy **cross-compilation** to all supported targets easy **cross-compilation** to all supported targets
# Requirements # Requirements
- A modern terminal with **24bit color** and, ideally, **kitty keyboard - A modern terminal with **24bit color** and, ideally, **kitty keyboard
protocol** support. **Kitty**, **Foot** and **Ghostty** are the protocol** support. **Kitty**, **Foot** and **Ghostty** are the recommended
recommended terminals at this time. **Zellij** also works well. Most terminals at this time. **Zellij** also works well. Most other terminals will
other terminals will work, but likely with reduced functionality. work, but likely with reduced functionality.
- **NerdFont** support. Either via terminal font fallback or a patched - **NerdFont** support. Either via terminal font fallback or a patched font.
font.
- A **UTF-8** locale - A **UTF-8** locale
# Roadmap # Roadmap
See our [devlog](https://flow-control.dev/devlog/2025/) for on-going See our [devlog](https://flow-control.dev/devlog/2025/) for on-going updates from the development team.
updates from the development team.
## In Development ## In Development
@ -67,8 +63,8 @@ updates from the development team.
# Download / Install # Download / Install
There is an [installation guide](https://flow-control.dev/installation) on There is an [installation guide](https://flow-control.dev/installation) on the
the main website, and source, release and nightly build binary main website, and source, release and nightly build binary
[downloads](https://flow-control.dev/downloads). [downloads](https://flow-control.dev/downloads).
Or check your favorite local system package repository. Or check your favorite local system package repository.
@ -86,9 +82,9 @@ Flow builds with zig 0.15.2 at this time. Build with:
zig build -Doptimize=ReleaseSafe zig build -Doptimize=ReleaseSafe
``` ```
Zig will by default build a binary optimized for your specific CPU. If you Zig will by default build a binary optimized for your specific CPU. If you get
get illegal instruction errors add `-Dcpu=baseline` to the build command to illegal instruction errors add `-Dcpu=baseline` to the build command to produce
produce a binary with generic CPU support. a binary with generic CPU support.
Thanks to Zig you may also cross-compile from any host to pretty much any Thanks to Zig you may also cross-compile from any host to pretty much any
@ -109,8 +105,8 @@ The output binary is:
zig-out/bin/flow zig-out/bin/flow
``` ```
It is statically built (by default) and contains all the required It is statically built (by default) and contains all the required tree-sitter parsers
tree-sitter parsers and queries. No additional runtime files are required. and queries. No additional runtime files are required.
# Running Flow Control # Running Flow Control
@ -129,9 +125,8 @@ Or if you prefer, let zig install it in your home directory:
zig build -Doptimize=ReleaseSafe --prefix ~/.local zig build -Doptimize=ReleaseSafe --prefix ~/.local
``` ```
Flow Control is a single statically linked binary. No further runtime files Flow Control is a single statically linked binary. No further runtime files are
are required. You may install it on another system by simply copying the required. You may install it on another system by simply copying the binary.
binary.
```shell ```shell
scp zig-out/bin/flow root@otherhost:/usr/local/bin scp zig-out/bin/flow root@otherhost:/usr/local/bin
@ -143,9 +138,8 @@ Files to load may be specifed on the command line:
flow fileA.zig fileB.zig flow fileA.zig fileB.zig
``` ```
The last file will be opened and the previous files will be placed in The last file will be opened and the previous files will be placed in reverse
reverse order at the top of the recent files list. Switch to recent files order at the top of the recent files list. Switch to recent files with Ctrl-e.
with Ctrl-e.
Common target line specifiers are supported too: Common target line specifiers are supported too:
@ -177,13 +171,13 @@ See `flow --help` for the full list of command line options.
A basic user manual is available inside flow. You can open it with the A basic user manual is available inside flow. You can open it with the
`Open help` command (F1). `Open help` command (F1).
It is also available in the website It is also available in the website [documentation](https://flow-control.dev/docs/)
[documentation](https://flow-control.dev/docs/) section. section.
## Development Resources ## Development Resources
Additional [developer](https://flow-control.dev/docs/#resources) resources Additional [developer](https://flow-control.dev/docs/#resources) resources can
can be found on the Flow Control website at. be found on the Flow Control website at.
There is also an AI generated developer guide at There is also an AI generated developer guide at
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/neurocyte/flow). [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/neurocyte/flow).
@ -192,23 +186,23 @@ Accuracy may vary. Check details against the referenced source code.
# Configuration # Configuration
Configuration is mostly dynamically maintained with various commands in the Configuration is mostly dynamically maintained with various commands in the UI.
UI. It is stored under the standard user configuration path. Usually It is stored under the standard user configuration path. Usually
`~/.config/flow` on Linux. %APPDATA%\Roaming\flow on Windows. Somewhere `~/.config/flow` on Linux. %APPDATA%\Roaming\flow on Windows. Somewhere magical
magical on MacOS. on MacOS.
There are commands to open the various configuration files, so you don't There are commands to open the various configuration files, so you don't have to
have to manually find them. Look for commands starting with `Edit` in the manually find them. Look for commands starting with `Edit` in the command
command palette. palette.
File types may be configured with the `Edit file type configuration` File types may be configured with the `Edit file type configuration` command.
command. You can also create a new file type by adding a new `.conf` file You can also create a new file type by adding a new `.conf` file to the
to the `file_type` directory. Have a look at an existing file type to see `file_type` directory. Have a look at an existing file type to see what options
what options are available. are available.
Logs, traces and per-project most recently used file lists are stored in Logs, traces and per-project most recently used file lists are stored in the
the standard user application state directory. Usually standard user application state directory. Usually `~/.local/state/flow` on
`~/.local/state/flow` on Linux and %APPDATA%\Roaming\flow on Windows. Linux and %APPDATA%\Roaming\flow on Windows.
# Key bindings and commands # Key bindings and commands
@ -219,16 +213,16 @@ Press `ctrl+shift+p` or `alt+x` to show the command palette.
Press `ctrl+F2` to see a full list of all current keybindings and commands. Press `ctrl+F2` to see a full list of all current keybindings and commands.
Run the `Edit keybindings` command to save the current keybinding mode to a Run the `Edit keybindings` command to save the current keybinding mode to a
file and open it for editing. Save your customized keybinds under a new file and open it for editing. Save your customized keybinds under a new name
name in the same directory to create an entirely new keybinding mode. in the same directory to create an entirely new keybinding mode. Keybinding
Keybinding changes will take effect on restart. changes will take effect on restart.
# Terminal configuration # Terminal configuration
Kitty, Ghostty and most other terminals have default keybindings that Kitty, Ghostty and most other terminals have default keybindings that conflict
conflict with common editor commands. I highly recommend rebinding them to with common editor commands. I highly recommend rebinding them to keys that are
keys that are not generally used anywhere else. not generally used anywhere else.
For Kitty rebinding `kitty_mod` is usually enough: For Kitty rebinding `kitty_mod` is usually enough:
``` ```

View file

@ -1,15 +1,10 @@
pub fn reflow(allocator: std.mem.Allocator, text: []const u8, width: usize) error{ OutOfMemory, WriteFailed }![]u8 { pub fn reflow(allocator: std.mem.Allocator, text: []const u8, width: usize) error{ OutOfMemory, WriteFailed }![]u8 {
const len = text.len; const prefix = detect_prefix(text);
const trailing_ln: bool = (len > 0 and text[len - 1] == '\n'); const words = try split_words(allocator, text, prefix.len);
const input = if (trailing_ln) text[0 .. len - 1] else text;
const prefix = detect_prefix(input);
const words = try split_words(allocator, input, prefix.len);
defer allocator.free(words); defer allocator.free(words);
var output: std.Io.Writer.Allocating = .init(allocator); var output: std.Io.Writer.Allocating = .init(allocator);
const writer = &output.writer; const writer = &output.writer;
std.log.info("reflow @{d}", .{width});
var first = true; var first = true;
var line_len: usize = 0; var line_len: usize = 0;
for (words) |word| { for (words) |word| {
@ -21,35 +16,30 @@ pub fn reflow(allocator: std.mem.Allocator, text: []const u8, width: usize) erro
.begin => { .begin => {
if (first) { if (first) {
try writer.writeAll(prefix.first); try writer.writeAll(prefix.first);
line_len += prefix.first.len;
first = false; first = false;
} else { } else {
try writer.writeAll(prefix.continuation); try writer.writeAll(prefix.continuation);
line_len += prefix.continuation.len;
var pad = prefix.first.len - prefix.continuation.len; var pad = prefix.first.len - prefix.continuation.len;
while (pad > 0) : (pad -= 1) { while (pad > 0) : (pad -= 1)
try writer.writeByte(' '); try writer.writeByte(' ');
line_len += 1;
}
} }
line_len += prefix.len;
continue :blk .words; continue :blk .words;
}, },
.words => { .words => {
if (line_len > prefix.len) { if (line_len > prefix.len and line_len + word.len + 1 >= width - 1) {
if (line_len + word.len + 1 >= width) {
try writer.writeByte('\n'); try writer.writeByte('\n');
line_len = 0; line_len = 0;
continue :blk .begin; continue :blk .begin;
} }
if (line_len > prefix.len)
try writer.writeByte(' '); try writer.writeByte(' ');
line_len += 1;
}
try writer.writeAll(word); try writer.writeAll(word);
line_len += word.len; line_len += word.len;
}, },
} }
} }
if (trailing_ln) try writer.writeByte('\n');
return output.toOwnedSlice(); return output.toOwnedSlice();
} }
@ -71,10 +61,10 @@ fn detect_prefix(text: []const u8) Prefix {
const line1 = lines.next() orelse return .{}; const line1 = lines.next() orelse return .{};
var prefix: []const u8 = line1; var prefix: []const u8 = line1;
var count: usize = 0; var count: usize = 0;
while (lines.next()) |line| if (line.len > 0) { while (lines.next()) |line| {
prefix = lcp(prefix, line); prefix = lcp(prefix, line);
count += 1; count += 1;
}; }
if (count < 1) return .{ if (count < 1) return .{
.len = 0, .len = 0,
.first = &.{}, .first = &.{},

View file

@ -33,7 +33,6 @@ follow_cursor_on_buffer_switch: bool = false, //scroll cursor into view on buffe
default_cursor: CursorShape = .default, default_cursor: CursorShape = .default,
modes_can_change_cursor: bool = true, modes_can_change_cursor: bool = true,
enable_auto_save: bool = false, enable_auto_save: bool = false,
auto_save_mode: AutoSaveMode = .on_focus_change,
limit_auto_save_file_types: ?[]const []const u8 = null, // null means *all* limit_auto_save_file_types: ?[]const []const u8 = null, // null means *all*
enable_prefix_keyhints: bool = true, enable_prefix_keyhints: bool = true,
enable_auto_find: bool = true, enable_auto_find: bool = true,
@ -46,7 +45,7 @@ auto_run_commands: ?[]const []const u8 = &.{"save_session_quiet"}, // a list of
indent_size: usize = 4, indent_size: usize = 4,
tab_width: usize = 8, tab_width: usize = 8,
indent_mode: IndentMode = .auto, indent_mode: IndentMode = .auto,
reflow_width: usize = 76, reflow_width: usize = 80,
top_bar: []const u8 = "tabs", top_bar: []const u8 = "tabs",
bottom_bar: []const u8 = "mode file log selection diagnostics keybind branch linenumber clock spacer", bottom_bar: []const u8 = "mode file log selection diagnostics keybind branch linenumber clock spacer",
@ -150,12 +149,6 @@ pub const WhitespaceMode = enum {
none, none,
}; };
pub const AutoSaveMode = enum {
on_input_idle,
on_focus_change,
on_document_change,
};
pub const CursorShape = enum { pub const CursorShape = enum {
default, default,
block_blink, block_blink,

View file

@ -649,7 +649,7 @@ pub const Editor = struct {
self.highlight_references_pending.deinit(self.allocator); self.highlight_references_pending.deinit(self.allocator);
self.handlers.deinit(); self.handlers.deinit();
self.logger.deinit(); self.logger.deinit();
if (self.buffer) |p| self.retire_buffer(p, meta.written()); if (self.buffer) |p| self.buffer_manager.retire(p, meta.written());
} }
pub fn need_render(_: *Self) void { pub fn need_render(_: *Self) void {
@ -697,6 +697,7 @@ pub const Editor = struct {
defer frame.deinit(); defer frame.deinit();
break :blk try self.buffer_manager.open_file(file_path); break :blk try self.buffer_manager.open_file(file_path);
}; };
if (tui.config().enable_auto_save) buffer.enable_auto_save();
return self.open_buffer(file_path, buffer, null); return self.open_buffer(file_path, buffer, null);
} }
@ -712,7 +713,7 @@ pub const Editor = struct {
fn open_buffer(self: *Self, file_path: []const u8, new_buf: *Buffer, file_type_: ?[]const u8) !void { fn open_buffer(self: *Self, file_path: []const u8, new_buf: *Buffer, file_type_: ?[]const u8) !void {
const frame = tracy.initZone(@src(), .{ .name = "open_buffer" }); const frame = tracy.initZone(@src(), .{ .name = "open_buffer" });
defer frame.deinit(); defer frame.deinit();
errdefer self.retire_buffer(new_buf, null); errdefer self.buffer_manager.retire(new_buf, null);
self.cancel_all_selections(); self.cancel_all_selections();
self.get_primary().reset(); self.get_primary().reset();
self.file_path = try self.allocator.dupe(u8, file_path); self.file_path = try self.allocator.dupe(u8, file_path);
@ -754,6 +755,8 @@ pub const Editor = struct {
self.checked_formatter = false; self.checked_formatter = false;
self.formatter = null; self.formatter = null;
self.maybe_enable_auto_save();
const syn = blk: { const syn = blk: {
const frame_ = tracy.initZone(@src(), .{ .name = "create" }); const frame_ = tracy.initZone(@src(), .{ .name = "create" });
defer frame_.deinit(); defer frame_.deinit();
@ -790,6 +793,7 @@ pub const Editor = struct {
buffer.file_type_icon = fti; buffer.file_type_icon = fti;
buffer.file_type_color = ftc; buffer.file_type_color = ftc;
} }
const auto_save = if (self.buffer) |b| b.is_auto_save() and !b.is_ephemeral() else false;
if (buffer_meta) |meta| { if (buffer_meta) |meta| {
const frame_ = tracy.initZone(@src(), .{ .name = "extract_state" }); const frame_ = tracy.initZone(@src(), .{ .name = "extract_state" });
@ -797,29 +801,24 @@ pub const Editor = struct {
var iter = meta; var iter = meta;
try self.extract_state(&iter, .none); try self.extract_state(&iter, .none);
} }
const auto_save = if (self.buffer) |b|
self.maybe_enable_buffer_auto_save(b)
else
false;
try self.send_editor_open(file_path, new_buf.file_exists, ftn, fti, ftc, auto_save); try self.send_editor_open(file_path, new_buf.file_exists, ftn, fti, ftc, auto_save);
} }
fn maybe_enable_buffer_auto_save(self: *Self, buffer: *Buffer) bool { fn maybe_enable_auto_save(self: *Self) void {
if (self.restored_state) return false; const buffer = self.buffer orelse return;
if (!tui.config().enable_auto_save) return false; if (self.restored_state) return;
const self_file_type = self.file_type orelse return false; buffer.disable_auto_save();
if (!tui.config().enable_auto_save) return;
const self_file_type = self.file_type orelse return;
enable: { enable: {
const file_types = tui.config().limit_auto_save_file_types orelse break :enable; const file_types = tui.config().limit_auto_save_file_types orelse break :enable;
for (file_types) |file_type| for (file_types) |file_type|
if (std.mem.eql(u8, file_type, self_file_type.name)) if (std.mem.eql(u8, file_type, self_file_type.name))
break :enable; break :enable;
return false; return;
} }
buffer.enable_auto_save(); buffer.enable_auto_save();
return !buffer.is_ephemeral();
} }
fn detect_indent_mode(self: *Self, content: []const u8) void { fn detect_indent_mode(self: *Self, content: []const u8) void {
@ -854,16 +853,11 @@ pub const Editor = struct {
} }
pub const set_editor_tab_width_meta: Meta = .{ .arguments = &.{.integer} }; pub const set_editor_tab_width_meta: Meta = .{ .arguments = &.{.integer} };
fn retire_buffer(self: *const Self, buffer: *Buffer, meta: ?[]const u8) void {
self.buffer_manager.retire(buffer, meta);
auto_save_buffer(buffer, .on_focus_change);
}
fn close(self: *Self) !void { fn close(self: *Self) !void {
var meta: std.Io.Writer.Allocating = .init(self.allocator); var meta: std.Io.Writer.Allocating = .init(self.allocator);
defer meta.deinit(); defer meta.deinit();
self.write_state(&meta.writer) catch {}; self.write_state(&meta.writer) catch {};
if (self.buffer) |b_mut| self.retire_buffer(b_mut, meta.written()); if (self.buffer) |b_mut| self.buffer_manager.retire(b_mut, meta.written());
self.cancel_all_selections(); self.cancel_all_selections();
self.buffer = null; self.buffer = null;
self.plane.erase(); self.plane.erase();
@ -2026,11 +2020,10 @@ pub const Editor = struct {
fn send_editor_update(self: *const Self, old_root: ?Buffer.Root, new_root: ?Buffer.Root, eol_mode: Buffer.EolMode) !void { fn send_editor_update(self: *const Self, old_root: ?Buffer.Root, new_root: ?Buffer.Root, eol_mode: Buffer.EolMode) !void {
_ = try self.handlers.msg(.{ "E", "update" }); _ = try self.handlers.msg(.{ "E", "update" });
if (self.buffer) |buffer| { if (self.buffer) |buffer| if (self.syntax) |_| if (self.file_path) |file_path| if (old_root != null and new_root != null)
if (self.syntax) |_| if (self.file_path) |file_path| if (old_root != null and new_root != null)
project_manager.did_change(file_path, buffer.lsp_version, try text_from_root(new_root, eol_mode), try text_from_root(old_root, eol_mode), eol_mode) catch {}; project_manager.did_change(file_path, buffer.lsp_version, try text_from_root(new_root, eol_mode), try text_from_root(old_root, eol_mode), eol_mode) catch {};
auto_save_buffer(buffer, .on_document_change); if (self.buffer) |b| if (b.is_auto_save() and !b.is_ephemeral())
} tp.self_pid().send(.{ "cmd", "save_file", .{} }) catch {};
} }
pub fn vcs_content_update(self: *const Self) !void { pub fn vcs_content_update(self: *const Self) !void {
@ -5554,10 +5547,7 @@ pub const Editor = struct {
buffer.disable_auto_save(); buffer.disable_auto_save();
self.send_editor_auto_save(buffer.is_auto_save()) catch {}; self.send_editor_auto_save(buffer.is_auto_save()) catch {};
if (buffer.is_auto_save()) if (buffer.is_auto_save())
std.log.info("enabled auto save {t}", .{tui.config().auto_save_mode}) tp.self_pid().send(.{ "cmd", "save_file", .{} }) catch {};
else
std.log.info("disabled auto save", .{});
auto_save_buffer(buffer, .on_document_change);
} }
pub const toggle_auto_save_meta: Meta = .{ .description = "Toggle auto save" }; pub const toggle_auto_save_meta: Meta = .{ .description = "Toggle auto save" };
@ -7145,7 +7135,6 @@ pub const EditorWidget = struct {
if (self.focused) self.commands.unregister(); if (self.focused) self.commands.unregister();
self.focused = false; self.focused = false;
command.executeName("enter_mode_default", .{}) catch {}; command.executeName("enter_mode_default", .{}) catch {};
if (self.editor.buffer) |b| auto_save_buffer(b, .on_focus_change);
} }
pub fn update(self: *Self) void { pub fn update(self: *Self) void {
@ -7224,7 +7213,6 @@ pub const EditorWidget = struct {
} }
}, },
}; };
if (self.editor.buffer) |b| auto_save_buffer(b, .on_input_idle);
return false; return false;
} else if (try m.match(.{ "whitespace_mode", tp.extract(&whitespace_mode) })) { } else if (try m.match(.{ "whitespace_mode", tp.extract(&whitespace_mode) })) {
self.editor.render_whitespace = whitespace_mode; self.editor.render_whitespace = whitespace_mode;
@ -7440,24 +7428,3 @@ fn ViewMap(T: type, default: T) type {
} }
}; };
} }
pub fn auto_save_buffer(b: *Buffer, comptime event: @import("config").AutoSaveMode) void {
const auto_save = switch (tui.config().auto_save_mode) {
.on_input_idle => comptime switch (event) {
.on_input_idle, .on_focus_change => true,
.on_document_change => false,
},
.on_document_change => comptime switch (event) {
.on_document_change => true,
.on_input_idle, .on_focus_change => false,
},
.on_focus_change => comptime switch (event) {
.on_focus_change => true,
.on_input_idle, .on_document_change => false,
},
};
if (auto_save and b.is_auto_save() and !b.is_ephemeral() and b.is_dirty()) {
tp.self_pid().send(.{ "cmd", "save_buffer", .{ b.get_file_path(), "auto_save" } }) catch {};
std.log.debug("auto save {t} {s}", .{ event, b.get_file_path() });
}
}

View file

@ -188,18 +188,10 @@ pub fn receive(self: *Self, from_: tp.pid_ref, m: tp.message) error{Exit}!bool {
} else if (try m.match(.{ "navigate_complete", tp.extract(&path), tp.extract(&goto_args), tp.null_, tp.null_ })) { } else if (try m.match(.{ "navigate_complete", tp.extract(&path), tp.extract(&goto_args), tp.null_, tp.null_ })) {
cmds.navigate_complete(self, null, path, goto_args, null, null, null) catch |e| return tp.exit_error(e, @errorReturnTrace()); cmds.navigate_complete(self, null, path, goto_args, null, null, null) catch |e| return tp.exit_error(e, @errorReturnTrace());
return true; return true;
} else if (try m.match(.{"focus_out"})) {
self.process_focus_out() catch |e| return tp.exit_error(e, @errorReturnTrace());
} }
return if (try self.floating_views.send(from_, m)) true else self.widgets.send(from_, m); return if (try self.floating_views.send(from_, m)) true else self.widgets.send(from_, m);
} }
fn process_focus_out(self: *Self) error{OutOfMemory}!void {
const buffers = try self.buffer_manager.list_unordered(self.allocator);
defer self.allocator.free(buffers);
for (buffers) |b| ed.auto_save_buffer(b, .on_focus_change);
}
pub fn update(self: *Self) void { pub fn update(self: *Self) void {
self.widgets.update(); self.widgets.update();
self.floating_views.update(); self.floating_views.update();
@ -777,11 +769,8 @@ const cmds = struct {
pub const create_new_file_meta: Meta = .{ .description = "New file" }; pub const create_new_file_meta: Meta = .{ .description = "New file" };
pub fn save_buffer(self: *Self, ctx: Ctx) Result { pub fn save_buffer(self: *Self, ctx: Ctx) Result {
var auto_save: bool = false;
var file_path: []const u8 = undefined; var file_path: []const u8 = undefined;
if (ctx.args.match(.{ tp.extract(&file_path), "auto_save" }) catch false) { if (!(ctx.args.match(.{tp.extract(&file_path)}) catch false))
auto_save = true;
} else if (!(ctx.args.match(.{tp.extract(&file_path)}) catch false))
return error.InvalidSaveBufferArgument; return error.InvalidSaveBufferArgument;
const buffer = self.buffer_manager.get_buffer_for_file(file_path) orelse return; const buffer = self.buffer_manager.get_buffer_for_file(file_path) orelse return;
@ -797,10 +786,7 @@ const cmds = struct {
const logger = log.logger("buffer"); const logger = log.logger("buffer");
defer logger.deinit(); defer logger.deinit();
if (buffer.is_ephemeral()) return logger.print_err("save", "ephemeral buffer, use save as", .{}); if (buffer.is_ephemeral()) return logger.print_err("save", "ephemeral buffer, use save as", .{});
if (!buffer.is_dirty()) { if (!buffer.is_dirty()) return logger.print("no changes to save", .{});
if (!auto_save) logger.print("no changes to save", .{});
return;
}
try buffer.store_to_file_and_clean(file_path); try buffer.store_to_file_and_clean(file_path);
} }
pub const save_buffer_meta: Meta = .{ .arguments = &.{.string} }; pub const save_buffer_meta: Meta = .{ .arguments = &.{.string} };

View file

@ -83,10 +83,6 @@ pub fn layout(_: *Self, _: *ButtonType) Widget.Layout {
} }
pub fn render(self: *Self, btn: *ButtonType, theme: *const Widget.Theme) bool { pub fn render(self: *Self, btn: *ButtonType, theme: *const Widget.Theme) bool {
const auto_save = if (self.auto_save) switch (tui.config().auto_save_mode) {
.on_input_idle, .on_document_change => true,
.on_focus_change => false,
} else false;
const style_base = theme.statusbar; const style_base = theme.statusbar;
const style_label = if (btn.active) theme.editor_cursor else style_base; const style_label = if (btn.active) theme.editor_cursor else style_base;
btn.plane.set_base_style(theme.editor); btn.plane.set_base_style(theme.editor);
@ -103,9 +99,9 @@ pub fn render(self: *Self, btn: *ButtonType, theme: *const Widget.Theme) bool {
if (tui.mini_mode()) |_| if (tui.mini_mode()) |_|
render_mini_mode(&btn.plane, theme) render_mini_mode(&btn.plane, theme)
else if (self.detailed) else if (self.detailed)
self.render_detailed(&btn.plane, theme, auto_save) self.render_detailed(&btn.plane, theme)
else else
self.render_normal(&btn.plane, theme, auto_save); self.render_normal(&btn.plane, theme);
self.render_terminal_title(); self.render_terminal_title();
return false; return false;
} }
@ -139,19 +135,19 @@ fn render_mini_mode(plane: *Plane, theme: *const Widget.Theme) void {
// 󱣪 Content save check // 󱣪 Content save check
// 󱑛 Content save cog // 󱑛 Content save cog
// 󰆔 Content save all // 󰆔 Content save all
fn render_normal(self: *Self, plane: *Plane, theme: *const Widget.Theme, auto_save: bool) void { fn render_normal(self: *Self, plane: *Plane, theme: *const Widget.Theme) void {
plane.on_styles(styles.italic); plane.on_styles(styles.italic);
_ = plane.putstr(" ") catch {}; _ = plane.putstr(" ") catch {};
if (self.file_icon.len > 0 and tui.config().show_fileicons) { if (self.file_icon.len > 0 and tui.config().show_fileicons) {
self.render_file_icon(plane, theme); self.render_file_icon(plane, theme);
_ = plane.print(" ", .{}) catch {}; _ = plane.print(" ", .{}) catch {};
} }
_ = plane.putstr(if (!self.file_exists) "󰽂 " else if (auto_save) "󱑛 " else if (self.file_dirty) "󰆓 " else "") catch {}; _ = plane.putstr(if (!self.file_exists) "󰽂 " else if (self.auto_save) "󱑛 " else if (self.file_dirty) "󰆓 " else "") catch {};
_ = plane.print("{s}", .{self.name}) catch {}; _ = plane.print("{s}", .{self.name}) catch {};
return; return;
} }
fn render_detailed(self: *Self, plane: *Plane, theme: *const Widget.Theme, auto_save: bool) void { fn render_detailed(self: *Self, plane: *Plane, theme: *const Widget.Theme) void {
plane.on_styles(styles.italic); plane.on_styles(styles.italic);
_ = plane.putstr(" ") catch {}; _ = plane.putstr(" ") catch {};
if (self.file_icon.len > 0 and tui.config().show_fileicons) { if (self.file_icon.len > 0 and tui.config().show_fileicons) {
@ -171,7 +167,7 @@ fn render_detailed(self: *Self, plane: *Plane, theme: *const Widget.Theme, auto_
.tabs => "[⭾ = ␉]", .tabs => "[⭾ = ␉]",
}; };
_ = plane.putstr(if (!self.file_exists) "󰽂" else if (auto_save) "󱑛" else if (self.file_dirty) "󰆓" else "󱣪") catch {}; _ = plane.putstr(if (!self.file_exists) "󰽂" else if (self.auto_save) "󱑛" else if (self.file_dirty) "󰆓" else "󱣪") catch {};
_ = plane.print(" {s}:{d}:{d}", .{ self.name, self.line + 1, self.column + 1 }) catch {}; _ = plane.print(" {s}:{d}:{d}", .{ self.name, self.line + 1, self.column + 1 }) catch {};
_ = plane.print(" of {d} lines", .{self.lines}) catch {}; _ = plane.print(" of {d} lines", .{self.lines}) catch {};
if (self.file_type.len > 0) if (self.file_type.len > 0)

View file

@ -902,10 +902,7 @@ const Tab = struct {
const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager"); const buffer_manager = tui.get_buffer_manager() orelse @panic("tabs no buffer manager");
const buffer_ = buffer_manager.buffer_from_ref(self.buffer_ref); const buffer_ = buffer_manager.buffer_from_ref(self.buffer_ref);
const is_dirty = if (buffer_) |buffer| buffer.is_dirty() else false; const is_dirty = if (buffer_) |buffer| buffer.is_dirty() else false;
const auto_save = if (buffer_) |buffer| if (buffer.is_auto_save()) switch (tui.config().auto_save_mode) { const auto_save = if (buffer_) |buffer| buffer.is_auto_save() else false;
.on_input_idle, .on_document_change => true,
.on_focus_change => false,
} else false else false;
self.render_padding(plane, .left); self.render_padding(plane, .left);
if (self.tab_style.file_type_icon) if (buffer_) |buffer| if (buffer.file_type_icon) |icon| { if (self.tab_style.file_type_icon) if (buffer_) |buffer| if (buffer.file_type_icon) |icon| {
const color_: ?u24 = if (buffer.file_type_color) |color| if (!(color == 0xFFFFFF or color == 0x000000 or color == 0x000001)) color else null else null; const color_: ?u24 = if (buffer.file_type_color) |color| if (!(color == 0xFFFFFF or color == 0x000000 or color == 0x000001)) color else null else null;

View file

@ -499,6 +499,12 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void {
if (try m.match(.{"mouse_leave"})) if (try m.match(.{"mouse_leave"}))
return; return;
if (try m.match(.{"focus_in"}))
return;
if (try m.match(.{"focus_out"}))
return;
if (try m.match(.{ "K", tp.more })) if (try m.match(.{ "K", tp.more }))
return; return;
@ -508,12 +514,6 @@ fn receive_safe(self: *Self, from: tp.pid_ref, m: tp.message) !void {
if (try self.send_widgets(from, m)) if (try self.send_widgets(from, m))
return; return;
if (try m.match(.{"focus_in"}))
return;
if (try m.match(.{"focus_out"}))
return;
if (try m.match(.{ "exit", tp.more })) { if (try m.match(.{ "exit", tp.more })) {
if (try m.match(.{ tp.string, "normal" }) or if (try m.match(.{ tp.string, "normal" }) or
try m.match(.{ tp.string, "timeout_error", 125, "Operation aborted." }) or try m.match(.{ tp.string, "timeout_error", 125, "Operation aborted." }) or