├── .gitignore ├── README.md ├── .github └── workflows │ └── ci.yml ├── LICENSE └── src ├── sane.zig ├── uefi.zig ├── example.zig └── main.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-out/ 2 | .zig-cache/ 3 | .vscode/ 4 | test.hist 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zigline 2 | 3 | A partial port of Serenity's [LibLine](https://github.com/SerenityOS/serenity/tree/master/Userland/Libraries/LibLine) in Zig. 4 | 5 | Have you ever looked at the current zig line editor options and thought "huh, these are all linenoise"? 6 | Well, look no further, this is not linenoise! 7 | 8 | LibLine is a full-featured terminal line editor with support for: 9 | 10 | - Flexible autocompletion 11 | - Live prompt and buffer update/stylisation 12 | - Multiline editing 13 | - and more. 14 | 15 | This port of LibLine is a work in progress, and as such, is incomplete. The following features are not yet implemented: 16 | - Masks 17 | - Many more small features 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Check format and build 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | check-format: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v5 14 | - uses: mlugg/setup-zig@v2 15 | with: 16 | version: 0.15.1 17 | - name: Check format 18 | run: zig fmt --check src 19 | 20 | build-native: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v5 24 | - uses: mlugg/setup-zig@v2 25 | with: 26 | version: 0.15.1 27 | - name: Build 28 | run: zig build 29 | 30 | build-cross: 31 | runs-on: ubuntu-latest 32 | strategy: 33 | matrix: 34 | target: [ "x86_64-linux-musl", "aarch64-macos-none", "x86-windows-gnu", "x86_64-uefi-gnu" ] 35 | steps: 36 | - uses: actions/checkout@v5 37 | - uses: mlugg/setup-zig@v2 38 | with: 39 | version: 0.15.1 40 | - name: Build 41 | run: zig build -Dtarget=${{ matrix.target }} 42 | 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2018-2023, the SerenityOS developers. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /src/sane.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | 4 | fn DeinitingAutoHashMap(comptime K: type, comptime V: type, comptime deinit_key: anytype, comptime deinit_value: anytype) type { 5 | return struct { 6 | container: std.AutoHashMap(K, V), 7 | 8 | const Self = @This(); 9 | pub const Entry = std.AutoHashMap(K, V).Entry; 10 | 11 | pub fn deinitEntries(self: *Self) void { 12 | var iterator = self.container.iterator(); 13 | while (iterator.next()) |entry| { 14 | deinit_value(entry.value_ptr); 15 | deinit_key(entry.key_ptr); 16 | } 17 | } 18 | 19 | pub fn init(allocator: Allocator) Self { 20 | return .{ 21 | .container = .init(allocator), 22 | }; 23 | } 24 | 25 | pub fn deinit(self: *Self) void { 26 | self.deinitEntries(); 27 | self.container.deinit(); 28 | } 29 | }; 30 | } 31 | 32 | fn DeinitingArrayList(comptime T: type, comptime deinit_fn: anytype) type { 33 | return struct { 34 | container: std.array_list.Managed(T), 35 | 36 | const Self = @This(); 37 | 38 | pub fn deinitEntries(self: *Self) void { 39 | for (self.container.items) |*item| { 40 | deinit_fn(item); 41 | } 42 | } 43 | 44 | pub fn init(allocator: Allocator) Self { 45 | return .{ 46 | .container = .init(allocator), 47 | }; 48 | } 49 | 50 | pub fn deinit(self: *Self) void { 51 | self.deinitEntries(); 52 | self.container.deinit(); 53 | } 54 | 55 | // ffs zig 56 | pub fn size(self: Self) usize { 57 | return self.container.items.len; 58 | } 59 | }; 60 | } 61 | 62 | fn deinitFnFor(comptime T: type) fn (ptr: *T) void { 63 | const t = @typeInfo(T); 64 | switch (t) { 65 | .@"struct" => { 66 | for (t.@"struct".decls) |decl| { 67 | if (std.mem.eql(u8, decl.name, "deinit")) { 68 | return T.deinit; 69 | } 70 | } 71 | }, 72 | else => {}, 73 | } 74 | 75 | return (struct { 76 | pub fn deinit(_: *T) void {} 77 | }).deinit; 78 | } 79 | 80 | pub fn AutoHashMap(comptime K: type, comptime V: type) type { 81 | return DeinitingAutoHashMap(K, V, deinitFnFor(K), deinitFnFor(V)); 82 | } 83 | 84 | pub fn ArrayList(comptime T: type) type { 85 | return DeinitingArrayList(T, deinitFnFor(T)); 86 | } 87 | 88 | pub fn Queue(comptime T: type) type { 89 | return struct { 90 | container: ArrayList(T), 91 | 92 | const Self = @This(); 93 | pub fn init(allocator: Allocator) Self { 94 | return .{ 95 | .container = .init(allocator), 96 | }; 97 | } 98 | 99 | pub fn deinit(self: *Self) void { 100 | self.container.deinit(); 101 | } 102 | 103 | pub fn enqueue(self: *Self, item: T) !void { 104 | try self.container.container.append(item); 105 | } 106 | 107 | pub fn dequeue(self: *Self) T { 108 | return self.container.container.orderedRemove(0); 109 | } 110 | 111 | pub fn isEmpty(self: Self) bool { 112 | return self.container.container.items.len == 0; 113 | } 114 | }; 115 | } 116 | 117 | fn Checkpoint(comptime T: type) type { 118 | return struct { 119 | v: T, 120 | p: *T, 121 | pub fn restore(self: @This()) void { 122 | self.p.* = self.v; 123 | } 124 | }; 125 | } 126 | 127 | fn checkpointImpl(comptime T: type, value: *T) Checkpoint(T) { 128 | return .{ 129 | .v = value.*, 130 | .p = value, 131 | }; 132 | } 133 | 134 | pub fn checkpoint(value: anytype) Checkpoint(@TypeOf(value.*)) { 135 | return checkpointImpl(@TypeOf(value.*), value); 136 | } 137 | 138 | pub const StringBuilder = struct { 139 | buffer: ArrayList(u8), 140 | 141 | const Self = @This(); 142 | 143 | pub fn init(allocator: Allocator) Self { 144 | return .{ 145 | .buffer = .init(allocator), 146 | }; 147 | } 148 | 149 | pub fn deinit(self: *Self) void { 150 | self.buffer.deinit(); 151 | } 152 | 153 | pub fn appendCodePoint(self: *Self, c: u32) !void { 154 | var buf: [4]u8 = @splat(0); 155 | const length = try std.unicode.utf8Encode(@intCast(c), &buf); 156 | try self.buffer.container.appendSlice(buf[0..length]); 157 | } 158 | 159 | pub fn appendSlice(self: *Self, slice: []const u8) !void { 160 | try self.buffer.container.appendSlice(slice); 161 | } 162 | 163 | pub fn appendUtf32Slice(self: *Self, slice: []const u32) !void { 164 | for (slice) |c| { 165 | try self.appendCodePoint(c); 166 | } 167 | } 168 | 169 | pub fn toOwnedSlice(self: *Self) ![]u8 { 170 | return self.buffer.container.toOwnedSlice(); 171 | } 172 | 173 | pub fn toSlice(self: *Self) []u8 { 174 | return self.buffer.container.items; 175 | } 176 | }; 177 | -------------------------------------------------------------------------------- /src/uefi.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub const Writer = struct { 4 | console_out: *std.os.uefi.protocol.SimpleTextOutput, 5 | attribute: std.os.uefi.protocol.SimpleTextOutput.Attribute, 6 | interface: std.Io.Writer, 7 | 8 | pub fn init( 9 | buffer: []u8, 10 | console_out: *std.os.uefi.protocol.SimpleTextOutput, 11 | attribute: std.os.uefi.protocol.SimpleTextOutput.Attribute, 12 | ) Writer { 13 | return .{ 14 | .console_out = console_out, 15 | .attribute = attribute, 16 | .interface = .{ 17 | .vtable = &.{ 18 | .drain = drain, 19 | }, 20 | .buffer = buffer, 21 | }, 22 | }; 23 | } 24 | 25 | const WriteError = 26 | std.os.uefi.protocol.SimpleTextOutput.SetAttributeError || 27 | std.os.uefi.protocol.SimpleTextOutput.OutputStringError; 28 | 29 | fn write(self: Writer, bytes: []const u8) WriteError!usize { 30 | try self.console_out.setAttribute(self.attribute); 31 | for (bytes) |c| { 32 | if (c == '\n') { 33 | _ = try self.console_out.outputString(&.{ '\r', 0 }); 34 | } 35 | _ = try self.console_out.outputString(&.{ c, 0 }); 36 | } 37 | return bytes.len; 38 | } 39 | 40 | fn drain(io_writer: *std.Io.Writer, data: []const []const u8, splat: usize) std.Io.Writer.Error!usize { 41 | const writer: *Writer = @alignCast(@fieldParentPtr("interface", io_writer)); 42 | const buffered = io_writer.buffered(); 43 | if (buffered.len != 0) { 44 | const n = writer.write(buffered) catch return error.WriteFailed; 45 | return io_writer.consume(n); 46 | } 47 | for (data[0 .. data.len - 1]) |buf| { 48 | if (buf.len == 0) continue; 49 | const n = writer.write(buf) catch return error.WriteFailed; 50 | return io_writer.consume(n); 51 | } 52 | const pattern = data[data.len - 1]; 53 | if (pattern.len == 0 or splat == 0) return 0; 54 | const n = writer.write(pattern) catch return error.WriteFailed; 55 | return io_writer.consume(n); 56 | } 57 | }; 58 | 59 | pub const Reader = struct { 60 | console_in: *std.os.uefi.protocol.SimpleTextInput, 61 | console_out: *std.os.uefi.protocol.SimpleTextOutput, 62 | attribute: std.os.uefi.protocol.SimpleTextOutput.Attribute, 63 | interface: std.Io.Reader, 64 | 65 | pub fn init( 66 | buffer: []u8, 67 | console_in: *std.os.uefi.protocol.SimpleTextInput, 68 | console_out: *std.os.uefi.protocol.SimpleTextOutput, 69 | attribute: std.os.uefi.protocol.SimpleTextOutput.Attribute, 70 | ) Reader { 71 | return .{ 72 | .console_in = console_in, 73 | .console_out = console_out, 74 | .attribute = attribute, 75 | .interface = .{ 76 | .vtable = &.{ 77 | .stream = stream, 78 | }, 79 | .buffer = buffer, 80 | .seek = 0, 81 | .end = 0, 82 | }, 83 | }; 84 | } 85 | 86 | const ReadError = 87 | std.os.uefi.protocol.SimpleTextInput.ReadKeyStrokeError || 88 | std.os.uefi.protocol.SimpleTextOutput.EnableCursorError || 89 | std.os.uefi.protocol.SimpleTextOutput.OutputStringError || 90 | std.os.uefi.protocol.SimpleTextOutput.SetAttributeError || 91 | std.os.uefi.tables.BootServices.WaitForEventError; 92 | 93 | fn read(self: Reader, bytes: []u8) ReadError!usize { 94 | try self.console_out.setAttribute(self.attribute); 95 | const boot_services = std.os.uefi.system_table.boot_services.?; 96 | var n: usize = 0; 97 | while (n < bytes.len) { 98 | _ = try boot_services.waitForEvent(&.{self.console_in.wait_for_key}); 99 | const key = try self.console_in.readKeyStroke(); 100 | if (key.scan_code != 0) continue; 101 | switch (key.unicode_char) { 102 | // Backspace 103 | 0x08 => |c| { 104 | // Only echo back to the output console if we have input buffered to not erase the prompt 105 | if (n == 0) continue; 106 | n -= 1; 107 | _ = try self.console_out.outputString(&.{ c, 0 }); 108 | }, 109 | '\r' => { 110 | bytes[n] = '\n'; 111 | n += 1; 112 | _ = try self.console_out.outputString(&.{ '\r', '\n', 0 }); 113 | break; 114 | }, 115 | else => |c| { 116 | n += std.unicode.utf8Encode(c, bytes[n..]) catch 0; 117 | _ = try self.console_out.outputString(&.{ c, 0 }); 118 | }, 119 | } 120 | } 121 | return n; 122 | } 123 | 124 | fn stream(io_reader: *std.Io.Reader, writer: *std.Io.Writer, limit: std.Io.Limit) std.Io.Reader.StreamError!usize { 125 | const reader: *Reader = @alignCast(@fieldParentPtr("interface", io_reader)); 126 | const dest = limit.slice(try writer.writableSliceGreedy(1)); 127 | const n = reader.read(dest) catch return error.ReadFailed; 128 | writer.advance(n); 129 | return n; 130 | } 131 | }; 132 | -------------------------------------------------------------------------------- /src/example.zig: -------------------------------------------------------------------------------- 1 | const Editor = @import("main.zig").Editor; 2 | const uefi = @import("uefi.zig"); 3 | const std = @import("std"); 4 | const builtin = @import("builtin"); 5 | 6 | pub const std_options: std.Options = switch (builtin.os.tag) { 7 | .uefi => .{ 8 | // std.log does not work on UEFI (yet) 9 | .logFn = struct { 10 | fn logFn( 11 | comptime _: std.log.Level, 12 | comptime _: @TypeOf(.enum_literal), 13 | comptime _: []const u8, 14 | _: anytype, 15 | ) void {} 16 | }.logFn, 17 | }, 18 | else => .{}, 19 | }; 20 | 21 | pub fn main() void { 22 | switch (builtin.os.tag) { 23 | .uefi => main_uefi(std.os.uefi.pool_allocator) catch {}, 24 | else => { 25 | var debug_allocator: std.heap.DebugAllocator(.{}) = .init; 26 | defer _ = debug_allocator.deinit(); 27 | main_generic(debug_allocator.allocator()) catch |err| { 28 | std.debug.print("Error: {t}\n", .{err}); 29 | }; 30 | }, 31 | } 32 | } 33 | 34 | fn main_generic(allocator: std.mem.Allocator) !void { 35 | var editor = Editor.init(allocator, .{}); 36 | defer editor.deinit(); 37 | 38 | try editor.loadHistory("test.hist"); 39 | defer editor.saveHistory("test.hist") catch unreachable; 40 | 41 | var handler: struct { 42 | editor: *Editor, 43 | completion_storage: [4]Editor.CompletionSuggestion = @splat(.{ .text = "" }), 44 | 45 | pub fn display_refresh(self: *@This()) void { 46 | self.editor.stripStyles(); 47 | const buf = self.editor.getBuffer(); 48 | for (buf, 0..) |c, i| { 49 | if (c > 0x7F) { 50 | self.editor.stylize(.{ 51 | .begin = i, 52 | .end = i + 1, 53 | }, .{ 54 | .foreground = .{ 55 | .rgb = .{ 255, 0, 0 }, 56 | }, 57 | }) catch unreachable; 58 | } else { 59 | self.editor.stylize(.{ 60 | .begin = i, 61 | .end = i + 1, 62 | }, .{ 63 | .foreground = .{ 64 | .rgb = .{ 65 | @intCast(c % 26 * 10), 66 | @intCast(c / 26 * 10), 67 | @intCast(c % 10 * 10 + 150), 68 | }, 69 | }, 70 | }) catch unreachable; 71 | } 72 | } 73 | } 74 | 75 | pub fn paste(self: *@This(), text: []const u32) void { 76 | self.editor.insertCodePoint('['); 77 | self.editor.insertUtf32(text); 78 | self.editor.insertCodePoint(']'); 79 | } 80 | 81 | pub fn tab_complete(self: *@This()) ![]const Editor.CompletionSuggestion { 82 | var count: usize = 1; 83 | self.completion_storage[0] = if (self.editor.cursor == 0) 84 | .{ 85 | .text = "t", 86 | .start_index = 0, 87 | } 88 | else b: { 89 | const next = switch (self.editor.getBuffer()[self.editor.cursor - 1]) { 90 | 't' => "e", 91 | 'e' => "s", 92 | 's' => "t", 93 | else => return &.{}, 94 | }; 95 | break :b .{ 96 | .text = next, 97 | .start_index = self.editor.cursor, 98 | .allow_commit_without_listing = false, 99 | }; 100 | }; 101 | switch (self.completion_storage[count - 1].text[0]) { 102 | 's' => { 103 | self.completion_storage[count] = .{ 104 | .text = "st", 105 | .start_index = self.editor.cursor, 106 | }; 107 | count += 1; 108 | }, 109 | 't' => { 110 | self.completion_storage[count] = .{ 111 | .text = "test2", 112 | .start_index = self.editor.cursor, 113 | }; 114 | count += 1; 115 | }, 116 | else => {}, 117 | } 118 | if (count == 2) { 119 | self.completion_storage[count] = .{ 120 | .text = "toast", 121 | .start_index = self.editor.cursor, 122 | }; 123 | count += 1; 124 | } 125 | 126 | return self.completion_storage[0..count]; 127 | } 128 | } = .{ .editor = &editor }; 129 | editor.setHandler(&handler); 130 | 131 | while (true) { 132 | const line: []const u8 = editor.getLine("> ") catch |err| switch (err) { 133 | error.Eof => break, 134 | else => return err, 135 | }; 136 | defer allocator.free(line); 137 | 138 | try editor.addToHistory(line); 139 | std.log.info("line ({d} bytes): {s}\n", .{ line.len, line }); 140 | 141 | if (std.mem.eql(u8, line, "quit")) { 142 | break; 143 | } 144 | } 145 | } 146 | 147 | fn main_uefi(allocator: std.mem.Allocator) !void { 148 | var editor = Editor.init(allocator, .{}); 149 | defer editor.deinit(); 150 | 151 | // kiesel: src/branch/main/src/uefi.zig 152 | const console_out = std.os.uefi.system_table.con_out.?; 153 | try console_out.reset(true); 154 | try console_out.clearScreen(); 155 | try console_out.enableCursor(true); 156 | 157 | var stdout_buffer: [1024]u8 = undefined; 158 | var stdout_writer: uefi.Writer = .init(&stdout_buffer, console_out, .{ .foreground = .white }); 159 | const stdout = &stdout_writer.interface; 160 | 161 | while (true) { 162 | const line: []const u8 = editor.getLine("> ") catch |err| switch (err) { 163 | error.Eof => break, 164 | else => return err, 165 | }; 166 | defer allocator.free(line); 167 | 168 | try editor.addToHistory(line); 169 | try stdout.print("line ({d} bytes): {s}\n", .{ line.len, line }); 170 | try stdout.flush(); 171 | 172 | if (std.mem.eql(u8, line, "quit")) { 173 | break; 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const Thread = std.Thread; 4 | const Condition = Thread.Condition; 5 | const Mutex = Thread.Mutex; 6 | 7 | const sane = @import("sane.zig"); 8 | const uefi = @import("uefi.zig"); 9 | const ArrayList = sane.ArrayList; 10 | const AutoHashMap = sane.AutoHashMap; 11 | const Queue = sane.Queue; 12 | const StringBuilder = sane.StringBuilder; 13 | 14 | const builtin = @import("builtin"); 15 | const is_windows = builtin.os.tag == .windows; 16 | const should_enable_signal_handling = switch (builtin.os.tag) { 17 | .windows, .wasi, .uefi => false, 18 | else => true, 19 | }; 20 | 21 | const logger = std.log.scoped(.zigline); 22 | 23 | const Module = @This(); 24 | 25 | fn Wrapped(comptime T: type) type { 26 | return struct { 27 | value: std.meta.Int(.unsigned, @bitSizeOf(T)), 28 | const t = T; 29 | }; 30 | } 31 | 32 | fn fromWrapped(value: anytype) @TypeOf(value).t { 33 | return @errorCast(@errorFromInt(value.value)); 34 | } 35 | 36 | fn toWrapped(comptime T: type, value: anytype) Wrapped(T) { 37 | return .{ .value = @intFromError(@as(T, @errorCast(value))) }; 38 | } 39 | 40 | pub const SystemCapabilities = switch (builtin.os.tag) { 41 | // FIXME: Windows' console handling is a mess, and std doesn't have 42 | // the necessary bindings to emulate termios on Windows. 43 | // For now, we just ignore the termios problem, which will lead to 44 | // garbled input; so we just ignore all that too and set the default 45 | // execution mode to "NonInteractive". 46 | .windows, 47 | .wasi, 48 | => struct { 49 | const Self = @This(); 50 | pub const Sigaction = struct {}; // This is not implemented in Zig, and we're not about to try. 51 | pub const pollfd = std.posix.pollfd; 52 | 53 | pub const termios = struct {}; 54 | pub const V = enum { 55 | EOF, 56 | EOL, 57 | ERASE, 58 | WERASE, 59 | INTR, 60 | KILL, 61 | MIN, 62 | QUIT, 63 | START, 64 | STOP, 65 | SUSP, 66 | TIME, 67 | }; 68 | 69 | pub const default_operation_mode: Configuration.OperationMode = .non_interactive; 70 | 71 | pub const stdin = std.fs.File.stdin; 72 | pub const stdout = std.fs.File.stdout; 73 | pub const stderr = std.fs.File.stderr; 74 | 75 | pub const winsize = struct { 76 | col: u16, 77 | row: u16, 78 | }; 79 | const getWinsize = if (builtin.os.tag == .windows) struct { 80 | pub fn getWinsize(handle: anytype) !winsize { 81 | var ws: std.posix.winsize = undefined; 82 | if (std.c.ioctl(handle, std.posix.T.IOCGWINSZ, @intFromPtr(&ws)) != 0 or 83 | ws.ws_col == 0 or 84 | ws.ws_row == 0) 85 | { 86 | const fd = std.posix.open("/dev/tty", .{ .ACCMODE = .RDONLY }, @as(std.posix.mode_t, 0)) catch return; 87 | if (fd != -1) { 88 | _ = std.c.ioctl(@intCast(fd), std.posix.T.IOCGWINSZ, @intFromPtr(&ws)); 89 | _ = std.posix.close(@intCast(fd)); 90 | } else { 91 | return error.Invalid; 92 | } 93 | } 94 | return .{ .col = ws.ws_col, .row = ws.ws_row }; 95 | } 96 | } else struct { 97 | pub fn getWinsize(handle: anytype) !winsize { 98 | _ = handle; 99 | return .{ .col = 80, .row = 24 }; 100 | } 101 | }.getWinsize; 102 | 103 | pub fn getTermios() !Self.termios { 104 | return .{}; 105 | } 106 | 107 | pub fn setTermios(_: Self.termios) !void {} 108 | 109 | pub fn clearEchoAndICanon(_: *Self.termios) void {} 110 | 111 | pub fn getTermiosCC(_: Self.termios, cc: Self.V) u8 { 112 | return switch (cc) { 113 | // Values aren't all that important, but the source is linux. 114 | .EOF => 4, 115 | .EOL => 0, 116 | .ERASE => 127, 117 | .WERASE => 23, // ctrl('w') 118 | .INTR => 3, 119 | .KILL => 21, 120 | .MIN => 1, 121 | .QUIT => 28, 122 | .START => 17, 123 | .STOP => 19, 124 | .SUSP => 26, 125 | .TIME => 0, 126 | }; 127 | } 128 | 129 | pub const POLL_IN = std.posix.POLL.RDNORM; 130 | 131 | pub fn setPollFd(p: *Self.pollfd, f: anytype) void { 132 | if (builtin.os.tag == .windows) { 133 | p.fd = @ptrCast(f); 134 | } else { 135 | p.fd = f; 136 | } 137 | } 138 | 139 | pub const nfds_t = std.posix.nfds_t; 140 | pub fn poll(fds: [*]Self.pollfd, n: nfds_t, timeout: i32) c_int { 141 | // std.posix.poll() has a Windows implementation but doesn't accept the second arg, only a slice. 142 | _ = timeout; 143 | fds[n - 1].revents = Self.POLL_IN; 144 | return 1; 145 | } 146 | 147 | const pipe = (if (is_windows) struct { 148 | pub fn pipe() ![2]std.posix.fd_t { 149 | var rd: std.os.windows.HANDLE = undefined; 150 | var wr: std.os.windows.HANDLE = undefined; 151 | var attrs: std.os.windows.SECURITY_ATTRIBUTES = undefined; 152 | attrs.nLength = 0; 153 | try std.os.windows.CreatePipe(&rd, &wr, &attrs); 154 | return .{ @ptrCast(rd), @ptrCast(wr) }; 155 | } 156 | } else struct { 157 | pub fn pipe() ![2]std.posix.fd_t { 158 | return std.posix.pipe(); 159 | } 160 | }).pipe; 161 | }, 162 | .uefi => struct { 163 | const Self = @This(); 164 | pub const Sigaction = struct {}; // No signal handling on UEFI :) 165 | pub const termios = struct {}; // FIXME: Implement? 166 | pub const V = enum { 167 | EOF, 168 | ERASE, 169 | WERASE, 170 | KILL, 171 | }; 172 | 173 | pub const pollfd = struct { 174 | fd: u32, 175 | events: i16, 176 | revents: i16, 177 | }; 178 | pub const nfds_t = u32; 179 | 180 | pub const default_operation_mode: Configuration.OperationMode = .non_interactive; 181 | 182 | const File = struct { 183 | handle: enum { 184 | stdin, 185 | stdout, 186 | stderr, 187 | }, 188 | 189 | pub fn reader(self: File, buffer: []u8) uefi.Reader { 190 | return switch (self.handle) { 191 | .stdin => .init( 192 | buffer, 193 | std.os.uefi.system_table.con_in.?, 194 | std.os.uefi.system_table.con_out.?, 195 | .{ .foreground = .white }, 196 | ), 197 | .stdout, .stderr => unreachable, 198 | }; 199 | } 200 | 201 | pub fn writer(self: File, buffer: []u8) uefi.Writer { 202 | return switch (self.handle) { 203 | .stdout => .init( 204 | buffer, 205 | std.os.uefi.system_table.con_out.?, 206 | .{ .foreground = .white }, 207 | ), 208 | .stderr => .init( 209 | buffer, 210 | std.os.uefi.system_table.con_out.?, 211 | .{ .foreground = .red }, 212 | ), 213 | .stdin => unreachable, 214 | }; 215 | } 216 | 217 | // Direct `read()` is only needed for poll so we can stub it out 218 | pub fn read(_: File, _: []u8) !usize { 219 | return 0; 220 | } 221 | }; 222 | 223 | pub fn stdin() File { 224 | return .{ .handle = .stdin }; 225 | } 226 | 227 | pub fn stdout() File { 228 | return .{ .handle = .stdout }; 229 | } 230 | 231 | pub fn stderr() File { 232 | return .{ .handle = .stderr }; 233 | } 234 | 235 | pub const winsize = struct { 236 | col: u16, 237 | row: u16, 238 | }; 239 | const getWinsize = struct { 240 | pub fn getWinsize(handle: anytype) !winsize { 241 | _ = handle; 242 | return .{ .col = 80, .row = 24 }; 243 | } 244 | }.getWinsize; 245 | 246 | pub fn getTermios() !Self.termios { 247 | return .{}; 248 | } 249 | 250 | pub fn setTermios(_: Self.termios) !void {} 251 | 252 | pub fn clearEchoAndICanon(_: *Self.termios) void {} 253 | 254 | pub fn getTermiosCC(_: Self.termios, cc: Self.V) u8 { 255 | return switch (cc) { 256 | // Values aren't all that important, but the source is linux. 257 | .EOF => 4, 258 | .ERASE => 127, 259 | .WERASE => 23, // ctrl('w') 260 | .KILL => 21, 261 | }; 262 | } 263 | 264 | pub const POLL_IN = 0; 265 | 266 | pub fn setPollFd(p: *Self.pollfd, f: anytype) void { 267 | _ = p; 268 | _ = f; 269 | } 270 | 271 | pub fn poll(fds: [*]Self.pollfd, n: nfds_t, timeout: i32) c_int { 272 | _ = timeout; 273 | fds[n - 1].revents = Self.POLL_IN; 274 | return 1; 275 | } 276 | 277 | const pipe = struct { 278 | pub fn pipe() ![2]std.posix.fd_t { 279 | return .{ 0, 0 }; 280 | } 281 | }.pipe; 282 | }, 283 | else => struct { 284 | const Self = @This(); 285 | pub const Sigaction = std.posix.Sigaction; 286 | pub const pollfd = std.posix.pollfd; 287 | 288 | pub const termios = std.posix.termios; 289 | 290 | pub const V = std.posix.V; 291 | 292 | pub const default_operation_mode: Configuration.OperationMode = .full; 293 | 294 | pub const stdin = std.fs.File.stdin; 295 | pub const stdout = std.fs.File.stdout; 296 | pub const stderr = std.fs.File.stderr; 297 | 298 | pub fn getWinsize(handle: anytype) !std.posix.winsize { 299 | var ws: std.posix.winsize = undefined; 300 | const ioctl = if (builtin.os.tag == .linux) std.os.linux.ioctl else std.c.ioctl; 301 | if (ioctl(handle, std.posix.T.IOCGWINSZ, @intFromPtr(&ws)) != 0 or 302 | ws.col == 0 or 303 | ws.row == 0) 304 | { 305 | const fd = try std.posix.open("/dev/tty", .{ .ACCMODE = .RDONLY }, @as(std.posix.mode_t, 0)); 306 | if (fd != -1) { 307 | _ = ioctl(@intCast(fd), std.posix.T.IOCGWINSZ, @intFromPtr(&ws)); 308 | _ = std.posix.close(@intCast(fd)); 309 | } else { 310 | return error.Invalid; 311 | } 312 | } 313 | 314 | return ws; 315 | } 316 | 317 | pub fn getTermios() !Self.termios { 318 | return try std.posix.tcgetattr(std.posix.STDIN_FILENO); 319 | } 320 | 321 | pub fn setTermios(t: Self.termios) !void { 322 | try std.posix.tcsetattr(std.posix.STDIN_FILENO, std.posix.TCSA.NOW, t); 323 | } 324 | 325 | pub fn clearEchoAndICanon(t: *Self.termios) void { 326 | t.lflag.ECHO = false; 327 | t.lflag.ICANON = false; 328 | t.lflag.ISIG = false; 329 | } 330 | 331 | pub fn getTermiosCC(t: Self.termios, cc: Self.V) u8 { 332 | return t.cc[@intFromEnum(cc)]; 333 | } 334 | 335 | pub const POLL_IN = std.posix.POLL.IN; 336 | 337 | pub fn setPollFd(p: *Self.pollfd, f: std.posix.fd_t) void { 338 | p.fd = f; 339 | } 340 | 341 | const PollReturnType = if (builtin.link_libc) c_int else usize; // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA 342 | pub const nfds_t = std.posix.nfds_t; 343 | pub fn poll(fds: [*]Self.pollfd, n: Self.nfds_t, timeout: i32) PollReturnType { 344 | return std.posix.system.poll(fds, n, timeout); 345 | } 346 | 347 | pub const pipe = std.posix.pipe; 348 | }, 349 | }; 350 | 351 | const getTermiosCC = SystemCapabilities.getTermiosCC; 352 | const getTermios = SystemCapabilities.getTermios; 353 | const setTermios = SystemCapabilities.setTermios; 354 | const clearEchoAndICanon = SystemCapabilities.clearEchoAndICanon; 355 | const termios = SystemCapabilities.termios; 356 | const V = SystemCapabilities.V; 357 | 358 | fn isAsciiControl(code_point: u32) bool { 359 | return code_point < 0x20 or code_point == 0x7f; 360 | } 361 | 362 | fn isAsciiSpace(code_point: u32) bool { 363 | return code_point == ' ' or code_point == '\t'; 364 | } 365 | 366 | fn isAsciiAlpha(code_point: u32) bool { 367 | return (code_point >= 'a' and code_point <= 'z') or (code_point >= 'A' and code_point <= 'Z'); 368 | } 369 | 370 | fn isAsciiNumeric(code_point: u32) bool { 371 | return code_point >= '0' and code_point <= '9'; 372 | } 373 | 374 | fn isAsciiAlnum(code_point: u32) bool { 375 | return isAsciiAlpha(code_point) or isAsciiNumeric(code_point); 376 | } 377 | 378 | pub fn ctrl(c: u32) u32 { 379 | return c & 0x3f; 380 | } 381 | 382 | fn utf8ValidRange(s: []const u8) usize { 383 | var len: usize = 0; 384 | 385 | const N = @sizeOf(usize); 386 | const MASK = 0x80 * (std.math.maxInt(usize) / 0xff); 387 | 388 | var i: usize = 0; 389 | while (i < s.len) { 390 | // Fast path for ASCII sequences 391 | while (i + N <= s.len) : (i += N) { 392 | const v = std.mem.readInt(usize, s[i..][0..N], builtin.cpu.arch.endian()); 393 | if (v & MASK != 0) break; 394 | len += N; 395 | } 396 | 397 | if (i < s.len) { 398 | const n = std.unicode.utf8ByteSequenceLength(s[i]) catch { 399 | return i; 400 | }; 401 | if (i + n > s.len) return i; 402 | 403 | switch (n) { 404 | 1 => {}, // ASCII, no validation needed 405 | else => _ = std.unicode.utf8Decode(s[i..][0..n]) catch { 406 | return i; 407 | }, 408 | } 409 | 410 | i += n; 411 | len += 1; 412 | } 413 | } 414 | 415 | return i; 416 | } 417 | 418 | pub const CompletionSuggestion = struct { 419 | text: []const u8, 420 | trailing_trivia: []const u8 = "", 421 | display_trivia: []const u8 = "", 422 | style: Style = .{}, 423 | start_index: usize = 0, 424 | input_offset: usize = 0, 425 | static_offset: usize = 0, 426 | invariant_offset: usize = 0, 427 | allow_commit_without_listing: bool = true, 428 | }; 429 | 430 | pub const Style = struct { 431 | underline: bool = false, 432 | bold: bool = false, 433 | italic: bool = false, 434 | background: Color = .{ .xterm = .unchanged }, 435 | foreground: Color = .{ .xterm = .unchanged }, 436 | // FIXME: Masks + Hyperlinks 437 | 438 | const Self = @This(); 439 | 440 | pub const reset: Style = .{ 441 | .background = .{ .xterm = .default }, 442 | .foreground = .{ .xterm = .default }, 443 | }; 444 | 445 | pub const XtermColor = enum(i32) { 446 | default = 9, 447 | black = 0, 448 | red, 449 | green, 450 | yellow, 451 | blue, 452 | magenta, 453 | cyan, 454 | white, 455 | unchanged, 456 | }; 457 | 458 | pub const Color = union(enum) { 459 | xterm: XtermColor, 460 | rgb: [3]u8, 461 | 462 | pub fn isDefault(self: @This()) bool { 463 | return switch (self) { 464 | .xterm => |xterm| xterm == .unchanged, 465 | .rgb => |rgb| rgb[0] == 0 and rgb[1] == 0 and rgb[2] == 0, 466 | }; 467 | } 468 | 469 | pub fn toVTEscape(self: @This(), allocator: Allocator, role: enum { background, foreground }) ![]const u8 { 470 | switch (self) { 471 | .xterm => |xterm| { 472 | if (xterm == .unchanged) { 473 | return ""; 474 | } 475 | return try std.fmt.allocPrint( 476 | allocator, 477 | "\x1b[{d}m", 478 | .{@intFromEnum(xterm) + @as(u8, if (role == .background) 40 else 30)}, 479 | ); 480 | }, 481 | .rgb => |rgb| { 482 | return try std.fmt.allocPrint( 483 | allocator, 484 | "\x1b[{};2;{d};{d};{d}m", 485 | .{ @as(u8, if (role == .background) 48 else 38), rgb[0], rgb[1], rgb[2] }, 486 | ); 487 | }, 488 | } 489 | } 490 | }; 491 | 492 | pub fn unifiedWith(self: Self, other: Self, prefer_other: bool) Self { 493 | var style = self; 494 | style.unifyWith(other, prefer_other); 495 | return style; 496 | } 497 | 498 | pub fn unifyWith(self: *Self, other: Self, prefer_other: bool) void { 499 | if (prefer_other or self.background.isDefault()) { 500 | self.background = other.background; 501 | } 502 | 503 | if (prefer_other or self.foreground.isDefault()) { 504 | self.foreground = other.foreground; 505 | } 506 | 507 | if (other.bold) { 508 | self.bold = true; 509 | } 510 | 511 | if (other.italic) { 512 | self.italic = true; 513 | } 514 | 515 | if (other.underline) { 516 | self.underline = true; 517 | } 518 | } 519 | }; 520 | pub const Span = struct { 521 | const Mode = enum { 522 | byte_oriented, 523 | code_point_oriented, 524 | }; 525 | const Self = @This(); 526 | 527 | begin: usize, 528 | end: usize, 529 | mode: Mode = .code_point_oriented, 530 | 531 | pub fn isEmpty(self: Self) bool { 532 | return self.begin >= self.end; 533 | } 534 | }; 535 | 536 | pub const StringMetrics = struct { 537 | pub const LineMetrics = struct { 538 | length: usize = 0, 539 | 540 | pub fn totalLength(self: @This()) usize { 541 | return self.length; 542 | } 543 | }; 544 | 545 | line_metrics: ArrayList(LineMetrics), 546 | total_length: usize = 0, 547 | max_line_length: usize = 0, 548 | 549 | const Self = @This(); 550 | 551 | pub fn init(allocator: Allocator) Self { 552 | return .{ 553 | .line_metrics = .init(allocator), 554 | }; 555 | } 556 | 557 | pub fn deinit(self: *Self) void { 558 | self.line_metrics.deinit(); 559 | } 560 | 561 | pub fn linesWithAddition(self: Self, offset: Self, column_width: usize) usize { 562 | var lines: usize = 0; 563 | 564 | if (self.line_metrics.size() != 0) { 565 | for (0..self.line_metrics.size() - 1) |i| { 566 | lines += (self.line_metrics.container.items[i].totalLength() + column_width) / column_width; 567 | } 568 | 569 | var last = self.line_metrics.container.items[self.line_metrics.size() - 1].totalLength(); 570 | last += offset.line_metrics.container.items[0].totalLength(); 571 | lines += (last + column_width) / column_width; 572 | } 573 | 574 | for (1..offset.line_metrics.size()) |i| { 575 | lines += (offset.line_metrics.container.items[i].totalLength() + column_width) / column_width; 576 | } 577 | 578 | return lines; 579 | } 580 | 581 | pub fn offsetWithAddition(self: Self, offset: Self, column_width: usize) usize { 582 | if (offset.line_metrics.size() > 1) { 583 | return offset.line_metrics.container.items[offset.line_metrics.size() - 1].totalLength() % column_width; 584 | } 585 | 586 | if (self.line_metrics.size() != 0) { 587 | var last = self.line_metrics.container.items[offset.line_metrics.size() - 1].totalLength(); 588 | last += offset.line_metrics.container.items[0].totalLength(); 589 | return last % column_width; 590 | } 591 | 592 | if (offset.line_metrics.size() == 0) { 593 | return 0; 594 | } 595 | 596 | return offset.line_metrics.container.items[0].totalLength() % column_width; 597 | } 598 | 599 | pub fn reset(self: *Self) !void { 600 | self.line_metrics.container.clearAndFree(); 601 | self.total_length = 0; 602 | self.max_line_length = 0; 603 | try self.line_metrics.container.append(.{}); 604 | } 605 | }; 606 | 607 | pub const SuggestionDisplay = struct { 608 | allocator: Allocator, 609 | origin_row: usize = 0, 610 | origin_column: usize = 0, 611 | is_showing: bool = false, 612 | lines: usize = 0, 613 | columns: usize = 0, 614 | lines_used_for_last_suggestion: usize = 0, 615 | prompt_lines_at_suggestion_initiation: usize = 0, 616 | pages: ArrayList(struct { start: usize, end: usize }), 617 | 618 | const Self = @This(); 619 | 620 | pub fn init(allocator: Allocator) Self { 621 | return .{ .allocator = allocator, .pages = .init(allocator) }; 622 | } 623 | 624 | pub fn deinit(self: *Self) void { 625 | self.pages.deinit(); 626 | } 627 | 628 | pub fn finish(self: *Self) void { 629 | self.pages.container.clearAndFree(); 630 | } 631 | 632 | pub fn cleanup(self: *Self) !bool { 633 | self.is_showing = false; 634 | if (self.lines_used_for_last_suggestion > 0) { 635 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 636 | const stderr = &stderr_writer.interface; 637 | // Save cursor position 638 | try stderr.print("\x1b7", .{}); 639 | // Move cursor to the beginning of the suggestion display 640 | try stderr.print("\x1b[{d};{d}H", .{ self.origin_row + self.prompt_lines_at_suggestion_initiation, 1 }); 641 | // Clear the suggestion display 642 | for (0..self.lines_used_for_last_suggestion) |_| { 643 | try stderr.print("\x1b[2K\x1b[1E", .{}); 644 | } 645 | // Restore cursor position 646 | try stderr.print("\x1b8", .{}); 647 | self.is_showing = false; 648 | return true; 649 | } 650 | 651 | return false; 652 | } 653 | 654 | pub fn display(self: *Self, manager: *SuggestionManager) !void { 655 | self.is_showing = true; 656 | 657 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 658 | const stderr = &stderr_writer.interface; 659 | 660 | var longest_suggestion_length: usize = 0; 661 | var longest_suggestion_length_without_trivia: usize = 0; 662 | 663 | for (manager.suggestions.container.items) |*s| { 664 | longest_suggestion_length = @max(longest_suggestion_length, s.text.len + s.trailing_trivia.len); 665 | longest_suggestion_length_without_trivia = @max(longest_suggestion_length_without_trivia, s.text.len); 666 | } 667 | 668 | var num_printed: usize = 0; 669 | var lines_used: usize = 1; 670 | 671 | _ = try self.cleanup(); 672 | 673 | var spans_entire_line: bool = false; 674 | const max_line_count = self.prompt_lines_at_suggestion_initiation + longest_suggestion_length / self.columns + @intFromBool(longest_suggestion_length % self.columns != 0); 675 | if (longest_suggestion_length >= self.columns - 2) { 676 | spans_entire_line = true; 677 | const start = max_line_count - self.prompt_lines_at_suggestion_initiation; 678 | // "reserve" space for the lines used. 679 | for (start..max_line_count) |_| { 680 | try stderr.print("\n", .{}); 681 | } 682 | lines_used += max_line_count; 683 | longest_suggestion_length = 0; 684 | } 685 | 686 | try vtMoveAbsolute(self.prompt_lines_at_suggestion_initiation + self.origin_row - 1, 0); 687 | 688 | if (self.pages.size() == 0) { 689 | var printed: usize = 0; 690 | var lines: usize = 1; 691 | var page_start: usize = 0; 692 | for (manager.suggestions.container.items, 0..) |*s, i| { 693 | const next_column = printed + s.text.len + longest_suggestion_length + 2; 694 | if (next_column > self.columns) { 695 | lines += (s.text.len + self.columns - 1) / self.columns; 696 | printed = 0; 697 | } 698 | if (lines + self.prompt_lines_at_suggestion_initiation > self.lines) { 699 | try self.pages.container.append(.{ .start = page_start, .end = i }); 700 | page_start = i; 701 | lines = 1; 702 | printed = 0; 703 | } 704 | printed += if (spans_entire_line) self.columns else longest_suggestion_length + 2; 705 | } 706 | 707 | try self.pages.container.append(.{ .start = page_start, .end = manager.suggestions.size() }); 708 | } 709 | 710 | const page_index = self.fit_to_page_boundary(manager.next_suggestion_index); 711 | const page = &self.pages.container.items[page_index]; 712 | for (manager.suggestions.container.items[page.start..page.end], page.start..page.end) |*s, i| { 713 | const next_column = num_printed + s.text.len + longest_suggestion_length + 2; 714 | if (next_column > self.columns) { 715 | lines_used += (s.text.len + self.columns - 1) / self.columns; 716 | num_printed = 0; 717 | try stderr.print("\n", .{}); 718 | } 719 | 720 | if (manager.last_shown_suggestion_was_complete and i == manager.next_suggestion_index) { 721 | try Editor.vtApplyStyle(.{ .foreground = .{ .xterm = .cyan } }, stderr, true); 722 | } 723 | 724 | if (spans_entire_line) { 725 | num_printed += self.columns; 726 | try stderr.print("{s}{s}", .{ s.text, s.display_trivia }); 727 | } else { 728 | const spaces_alloc: [256]u8 = @splat(' '); 729 | const spaces = spaces_alloc[0 .. longest_suggestion_length - s.text.len - s.trailing_trivia.len]; 730 | num_printed += longest_suggestion_length + 2; 731 | try stderr.print("{s}{s}{s} ", .{ s.text, spaces, s.display_trivia }); 732 | } 733 | 734 | if (manager.last_shown_suggestion_was_complete and i == manager.next_suggestion_index) { 735 | try Editor.vtApplyStyle(.reset, stderr, true); 736 | } 737 | } 738 | 739 | self.lines_used_for_last_suggestion = lines_used; 740 | lines_used += self.prompt_lines_at_suggestion_initiation - 1; 741 | 742 | if (self.origin_row + lines_used > self.lines) { 743 | self.origin_row = self.lines - lines_used; 744 | } 745 | } 746 | 747 | fn fit_to_page_boundary(self: *Self, index: usize) usize { 748 | for (self.pages.container.items, 0..) |*page, i| { 749 | if (index >= page.start and index < page.end) { 750 | return i; 751 | } 752 | } 753 | return 0; 754 | } 755 | }; 756 | pub const SuggestionManager = struct { 757 | allocator: Allocator, 758 | suggestions: ArrayList(CompletionSuggestion), 759 | last_shown_suggestion: CompletionSuggestion = .{ .text = "" }, 760 | last_shown_suggestion_display_length: usize = 0, 761 | last_shown_suggestion_was_complete: bool = false, 762 | next_suggestion_index: usize = 0, 763 | largest_common_suggestion_prefix_length: usize = 0, 764 | last_displayed_suggestion_index: usize = 0, 765 | selected_suggestion_index: usize = 0, 766 | 767 | const Self = @This(); 768 | pub const CompletionMode = enum(u8) { 769 | @"don't_complete", 770 | complete_prefix, 771 | show_suggestions, 772 | cycle_suggestions, 773 | }; 774 | 775 | pub const CompletionAttemptResult = struct { 776 | new_mode: CompletionMode, 777 | new_cursor_offset: isize = 0, 778 | // Region to remove: [start, end) translated by (old_cursor + new_cursor_offset). 779 | offset_region_to_remove: struct { start: usize, end: usize } = .{ .start = 0, .end = 0 }, 780 | // Range to restore after rejection of this suggestion. 781 | static_offset_from_cursor: usize = 0, 782 | insert_storage: [8][]const u8 = @splat(undefined), 783 | insert_count: usize = 0, 784 | style_to_apply: ?Style = null, 785 | avoid_committing_to_single_suggestion: bool = false, 786 | }; 787 | 788 | pub fn init(allocator: Allocator) Self { 789 | return .{ 790 | .allocator = allocator, 791 | .suggestions = .init(allocator), 792 | }; 793 | } 794 | 795 | pub fn deinit(self: *Self) void { 796 | self.suggestions.deinit(); 797 | } 798 | 799 | fn commonPrefixLength(comptime T: type, a: []const T, b: []const T) usize { 800 | const min_len = @min(a.len, b.len); 801 | var i: usize = 0; 802 | while (i < min_len) : (i += 1) { 803 | if (a[i] != b[i]) break; 804 | } 805 | return i; 806 | } 807 | 808 | pub fn setSuggestions(self: *Self, suggestions: []const CompletionSuggestion) void { 809 | self.suggestions.container.clearAndFree(); 810 | self.next_suggestion_index = 0; 811 | self.largest_common_suggestion_prefix_length = 0; 812 | self.last_displayed_suggestion_index = 0; 813 | self.selected_suggestion_index = 0; 814 | 815 | for (suggestions) |s| { 816 | _ = self.suggestions.container.append(s) catch unreachable; // TODO 817 | } 818 | 819 | if (self.suggestions.size() > 1) { 820 | var prefix_len = @min(self.suggestions.container.items[0].text.len, self.suggestions.container.items[1].text.len); 821 | for (2..self.suggestions.size()) |i| { 822 | prefix_len = @min( 823 | prefix_len, 824 | commonPrefixLength( 825 | u8, 826 | self.suggestions.container.items[0].text, 827 | self.suggestions.container.items[i].text, 828 | ), 829 | ); 830 | } 831 | self.largest_common_suggestion_prefix_length = prefix_len; 832 | } else if (self.suggestions.size() == 1) { 833 | self.largest_common_suggestion_prefix_length = self.suggestions.container.items[0].text.len; 834 | } 835 | } 836 | 837 | fn suggest(self: *Self) *CompletionSuggestion { 838 | const suggestion = &self.suggestions.container.items[self.next_suggestion_index]; 839 | self.last_shown_suggestion = suggestion.*; 840 | return suggestion; 841 | } 842 | 843 | pub fn setCurrentSuggestionInitiationIndex(self: *Self, index: usize) void { 844 | const suggestion = self.suggest(); 845 | self.last_shown_suggestion.start_index = if (self.last_shown_suggestion_display_length > 0) 846 | index - suggestion.static_offset - self.last_shown_suggestion_display_length 847 | else 848 | index - suggestion.static_offset - suggestion.invariant_offset; 849 | 850 | self.last_shown_suggestion_display_length = suggestion.text.len; 851 | self.last_shown_suggestion_was_complete = true; 852 | } 853 | 854 | pub fn attemptCompletion(self: *Self, mode: CompletionMode, initiation_index: usize) CompletionAttemptResult { 855 | var result = CompletionAttemptResult{ .new_mode = mode }; 856 | 857 | if (self.next_suggestion_index < self.suggestions.size()) { 858 | const next_suggestion = &self.suggestions.container.items[self.next_suggestion_index]; 859 | if (mode == .complete_prefix and !next_suggestion.allow_commit_without_listing) { 860 | result.new_mode = .show_suggestions; 861 | result.avoid_committing_to_single_suggestion = true; 862 | self.last_shown_suggestion_display_length = 0; 863 | self.last_shown_suggestion_was_complete = false; 864 | self.last_shown_suggestion = .{ .text = "" }; 865 | return result; 866 | } 867 | 868 | const can_complete = next_suggestion.invariant_offset <= self.largest_common_suggestion_prefix_length; 869 | var actual_offset: isize = 0; 870 | var shown_length = self.last_shown_suggestion_display_length; 871 | switch (mode) { 872 | .complete_prefix => { 873 | actual_offset = 0; 874 | }, 875 | .show_suggestions => { 876 | actual_offset = @intCast(next_suggestion.invariant_offset - self.largest_common_suggestion_prefix_length); 877 | if (can_complete and next_suggestion.allow_commit_without_listing) { 878 | shown_length = self.largest_common_suggestion_prefix_length + self.last_shown_suggestion.trailing_trivia.len; 879 | } 880 | }, 881 | else => { 882 | if (self.last_shown_suggestion_display_length != 0) { 883 | actual_offset = @as(isize, @intCast(next_suggestion.invariant_offset)) - @as(isize, @intCast(self.last_shown_suggestion.text.len)); 884 | } 885 | }, 886 | } 887 | 888 | const suggestion = self.suggest(); 889 | self.setCurrentSuggestionInitiationIndex(initiation_index); 890 | 891 | result.offset_region_to_remove = .{ 892 | .start = next_suggestion.invariant_offset, 893 | .end = shown_length, 894 | }; 895 | result.new_cursor_offset = actual_offset; 896 | result.static_offset_from_cursor = suggestion.static_offset; 897 | 898 | if (mode == .complete_prefix) { 899 | if (can_complete) { 900 | result.insert_storage[result.insert_count] = suggestion.text[suggestion.invariant_offset..self.largest_common_suggestion_prefix_length]; 901 | result.insert_count += 1; 902 | self.last_shown_suggestion_display_length = self.largest_common_suggestion_prefix_length + suggestion.trailing_trivia.len; 903 | if (self.suggestions.size() == 1) { 904 | result.new_mode = .@"don't_complete"; 905 | result.insert_storage[result.insert_count] = suggestion.trailing_trivia; 906 | result.insert_count += 1; 907 | self.last_shown_suggestion_display_length = 0; 908 | result.style_to_apply = suggestion.style; 909 | self.last_shown_suggestion_was_complete = true; 910 | return result; 911 | } 912 | } else { 913 | self.last_shown_suggestion_display_length = 0; 914 | } 915 | result.new_mode = .show_suggestions; 916 | self.last_shown_suggestion_was_complete = false; 917 | self.last_shown_suggestion = .{ .text = "" }; 918 | } else { 919 | result.insert_storage[result.insert_count] = suggestion.text[suggestion.invariant_offset..]; 920 | result.insert_count += 1; 921 | result.insert_storage[result.insert_count] = suggestion.trailing_trivia; 922 | result.insert_count += 1; 923 | self.last_shown_suggestion_display_length += suggestion.trailing_trivia.len; 924 | } 925 | } else { 926 | self.next_suggestion_index = 0; 927 | } 928 | 929 | return result; 930 | } 931 | 932 | pub fn next(self: *Self) void { 933 | if (self.suggestions.size() == 0) return; 934 | self.next_suggestion_index = (self.next_suggestion_index + 1) % self.suggestions.size(); 935 | } 936 | 937 | pub fn previous(self: *Self) void { 938 | if (self.suggestions.size() == 0) return; 939 | if (self.next_suggestion_index == 0) { 940 | self.next_suggestion_index = self.suggestions.size() - 1; 941 | } else { 942 | self.next_suggestion_index -= 1; 943 | } 944 | } 945 | 946 | pub fn reset(self: *Self) void { 947 | self.suggestions.container.clearAndFree(); 948 | self.next_suggestion_index = 0; 949 | self.largest_common_suggestion_prefix_length = 0; 950 | self.last_displayed_suggestion_index = 0; 951 | self.selected_suggestion_index = 0; 952 | self.last_shown_suggestion = .{ .text = "" }; 953 | self.last_shown_suggestion_display_length = 0; 954 | self.last_shown_suggestion_was_complete = false; 955 | } 956 | }; 957 | pub const CSIMod = enum(u8) { 958 | none = 0, 959 | shift = 1, 960 | alt = 2, 961 | ctrl = 4, 962 | }; 963 | pub const Key = struct { 964 | code_point: u32, 965 | modifiers: enum(u8) { 966 | none = 0, 967 | alt = 1, 968 | } = .none, 969 | 970 | const Self = @This(); 971 | 972 | pub fn equals(self: Self, other: Self) bool { 973 | return self.code_point == other.code_point and self.modifiers == other.modifiers; 974 | } 975 | }; 976 | 977 | const KeyCallbackEntry = struct { 978 | sequence: ArrayList(Key), 979 | callback: *const fn (*Editor) bool, 980 | 981 | const Self = @This(); 982 | 983 | pub fn init(allocator: Allocator, sequence: []const Key, f: *const fn (*Editor) bool) Self { 984 | var self: Self = .{ 985 | .sequence = .init(allocator), 986 | .callback = f, 987 | }; 988 | self.sequence.container.appendSlice(sequence) catch unreachable; 989 | return self; 990 | } 991 | 992 | pub fn deinit(self: *Self) void { 993 | self.sequence.deinit(); 994 | } 995 | }; 996 | 997 | pub const KeyCallbackMachine = struct { 998 | key_callbacks: ArrayList(KeyCallbackEntry), 999 | current_matching_keys: ArrayList([]const Key), 1000 | sequence_length: usize = 0, 1001 | should_process_this_key: bool = true, 1002 | 1003 | const Self = @This(); 1004 | 1005 | pub fn init(allocator: Allocator) Self { 1006 | return .{ 1007 | .key_callbacks = .init(allocator), 1008 | .current_matching_keys = .init(allocator), 1009 | }; 1010 | } 1011 | 1012 | pub fn deinit(self: *Self) void { 1013 | self.key_callbacks.deinit(); 1014 | self.current_matching_keys.deinit(); 1015 | } 1016 | 1017 | pub fn keyPressed(self: *Self, editor: *Editor, key: Key) !void { 1018 | if (self.sequence_length == 0) { 1019 | std.debug.assert(self.current_matching_keys.size() == 0); 1020 | 1021 | for (self.key_callbacks.container.items) |entry| { 1022 | if (entry.sequence.container.items[0].equals(key)) { 1023 | try self.current_matching_keys.container.append(entry.sequence.container.items); 1024 | } 1025 | } 1026 | 1027 | if (self.current_matching_keys.size() == 0) { 1028 | self.should_process_this_key = true; 1029 | return; 1030 | } 1031 | } 1032 | 1033 | self.sequence_length += 1; 1034 | var old_matching_keys: ArrayList([]const Key) = .init(editor.allocator); 1035 | std.mem.swap(@TypeOf(old_matching_keys), &old_matching_keys, &self.current_matching_keys); 1036 | defer old_matching_keys.deinit(); 1037 | 1038 | for (old_matching_keys.container.items) |o_key| { 1039 | if (o_key.len < self.sequence_length) { 1040 | continue; 1041 | } 1042 | 1043 | if (o_key[self.sequence_length - 1].equals(key)) { 1044 | try self.current_matching_keys.container.append(o_key); 1045 | } 1046 | } 1047 | 1048 | if (self.current_matching_keys.size() == 0) { 1049 | // Insert any captured keys 1050 | if (old_matching_keys.size() != 0) { 1051 | for (old_matching_keys.container.items[0]) |k| { 1052 | editor.insertCodePoint(k.code_point); 1053 | } 1054 | } 1055 | self.sequence_length = 0; 1056 | self.should_process_this_key = true; 1057 | return; 1058 | } 1059 | 1060 | self.should_process_this_key = false; 1061 | for (self.current_matching_keys.container.items) |ks| { 1062 | if (ks.len == self.sequence_length) { 1063 | self.should_process_this_key = self.callback(ks, editor, false); 1064 | self.sequence_length = 0; 1065 | self.current_matching_keys.container.clearAndFree(); 1066 | return; 1067 | } 1068 | } 1069 | } 1070 | 1071 | fn callback(self: *Self, keys: []const Key, editor: *Editor, default: bool) bool { 1072 | for (self.key_callbacks.container.items) |entry| { 1073 | if (entry.sequence.container.items.len == keys.len) { 1074 | var match = true; 1075 | for (entry.sequence.container.items, 0..keys.len) |key, i| { 1076 | if (!key.equals(keys[i])) { 1077 | match = false; 1078 | break; 1079 | } 1080 | } 1081 | if (match) { 1082 | return entry.callback(editor); 1083 | } 1084 | } 1085 | } 1086 | 1087 | return default; 1088 | } 1089 | 1090 | pub fn registerKeyInputCallback(self: *Self, sequence: []const Key, c: *const fn (*Editor) bool) !void { 1091 | const inserted_entry: KeyCallbackEntry = .init(self.key_callbacks.container.allocator, sequence, c); 1092 | 1093 | for (self.key_callbacks.container.items, 0..self.key_callbacks.size()) |entry, j| { 1094 | if (entry.sequence.container.items.len == sequence.len) { 1095 | var match = true; 1096 | for (entry.sequence.container.items, 0..sequence.len) |key, i| { 1097 | if (!key.equals(sequence[i])) { 1098 | match = false; 1099 | break; 1100 | } 1101 | } 1102 | if (match) { 1103 | self.key_callbacks.container.items[j].deinit(); 1104 | self.key_callbacks.container.items[j] = inserted_entry; 1105 | return; 1106 | } 1107 | } 1108 | } 1109 | 1110 | try self.key_callbacks.container.append(inserted_entry); 1111 | } 1112 | 1113 | pub fn shouldProcessLastPressedKey(self: *Self) bool { 1114 | return self.should_process_this_key; 1115 | } 1116 | }; 1117 | pub const HistoryEntry = struct { 1118 | allocator: Allocator, 1119 | entry: []const u8, 1120 | timestamp: i64, 1121 | 1122 | const Self = @This(); 1123 | 1124 | pub fn init(allocator: Allocator, entry: []const u8) !Self { 1125 | return .{ 1126 | .allocator = allocator, 1127 | .entry = try allocator.dupe(u8, entry), 1128 | .timestamp = std.time.timestamp(), 1129 | }; 1130 | } 1131 | 1132 | pub fn initWithTimestamp(allocator: Allocator, entry: []const u8, timestamp: i64) !Self { 1133 | return .{ 1134 | .allocator = allocator, 1135 | .entry = try allocator.dupe(u8, entry), 1136 | .timestamp = timestamp, 1137 | }; 1138 | } 1139 | 1140 | pub fn deinit(self: *Self) void { 1141 | self.allocator.free(self.entry); 1142 | } 1143 | }; 1144 | pub const Configuration = struct { 1145 | pub const OperationMode = enum { 1146 | full, 1147 | no_escape_sequences, 1148 | non_interactive, 1149 | }; 1150 | 1151 | enable_bracketed_paste: bool = true, 1152 | operation_mode: OperationMode = SystemCapabilities.default_operation_mode, 1153 | enable_signal_handling: bool = should_enable_signal_handling, 1154 | }; 1155 | 1156 | fn vtMoveRelative(row: i64, col: i64) !void { 1157 | var x_op: u8 = 'A'; 1158 | var y_op: u8 = 'D'; 1159 | var r = row; 1160 | var c = col; 1161 | 1162 | if (row > 0) { 1163 | x_op = 'B'; 1164 | } else { 1165 | r = -row; 1166 | } 1167 | 1168 | if (col > 0) { 1169 | y_op = 'C'; 1170 | } else { 1171 | c = -col; 1172 | } 1173 | 1174 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 1175 | const stderr = &stderr_writer.interface; 1176 | if (row > 0) { 1177 | try stderr.print("\x1b[{d}{c}", .{ r, x_op }); 1178 | } 1179 | if (col > 0) { 1180 | try stderr.print("\x1b[{d}{c}", .{ c, y_op }); 1181 | } 1182 | } 1183 | 1184 | fn vtMoveAbsolute(row: usize, col: usize) !void { 1185 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 1186 | const stderr = &stderr_writer.interface; 1187 | try stderr.print("\x1b[{d};{d}H", .{ row + 1, col + 1 }); 1188 | } 1189 | 1190 | var signalHandlingData: ?struct { 1191 | pipe: struct { 1192 | write: std.posix.fd_t, 1193 | read: std.posix.fd_t, 1194 | }, 1195 | old_sigint: ?SystemCapabilities.Sigaction = null, 1196 | old_sigwinch: ?SystemCapabilities.Sigaction = null, 1197 | 1198 | pub fn handleSignal(signo: i32) callconv(.c) void { 1199 | var file: std.fs.File = .{ .handle = signalHandlingData.?.pipe.write }; 1200 | var buffer: [4]u8 = undefined; 1201 | var file_writer = file.writer(&buffer); 1202 | const writer = &file_writer.interface; 1203 | writer.writeInt(i32, signo, .little) catch {}; 1204 | } 1205 | } = null; 1206 | 1207 | pub const Editor = struct { 1208 | pub const Signal = enum { 1209 | SIGWINCH, 1210 | }; 1211 | 1212 | const Self = @This(); 1213 | const InputState = enum { 1214 | free, 1215 | verbatim, 1216 | paste, 1217 | got_escape, 1218 | csi_expect_parameter, 1219 | csi_expect_intermediate, 1220 | csi_expect_final, 1221 | }; 1222 | const VTState = enum { 1223 | free, 1224 | escape, 1225 | bracket, 1226 | bracket_args_semi, 1227 | title, 1228 | }; 1229 | pub const CompletionSuggestion = Module.CompletionSuggestion; 1230 | const DrawnSpans = struct { 1231 | starting: AutoHashMap(usize, AutoHashMap(usize, Style)), 1232 | ending: AutoHashMap(usize, AutoHashMap(usize, Style)), 1233 | 1234 | pub fn init(allocator: Allocator) @This() { 1235 | return .{ 1236 | .starting = .init(allocator), 1237 | .ending = .init(allocator), 1238 | }; 1239 | } 1240 | 1241 | pub fn deinit(self: *@This()) void { 1242 | self.starting.deinit(); 1243 | self.ending.deinit(); 1244 | } 1245 | 1246 | pub fn containsUpToOffset(self: @This(), other: @This(), offset: usize) bool { 1247 | _ = self; 1248 | _ = other; 1249 | _ = offset; 1250 | return false; 1251 | } 1252 | 1253 | pub fn deepCopy(self: *const @This()) !@This() { 1254 | var other: @This() = .init(self.starting.container.allocator); 1255 | var start_it = self.starting.container.iterator(); 1256 | while (start_it.next()) |start| { 1257 | var inner_it = start.value_ptr.container.iterator(); 1258 | var hm: AutoHashMap(usize, Style) = .init(self.starting.container.allocator); 1259 | while (inner_it.next()) |inner| { 1260 | try hm.container.put(inner.key_ptr.*, inner.value_ptr.*); 1261 | } 1262 | try other.starting.container.put(start.key_ptr.*, hm); 1263 | } 1264 | 1265 | var end_it = self.ending.container.iterator(); 1266 | while (end_it.next()) |end| { 1267 | var inner_it = end.value_ptr.container.iterator(); 1268 | var hm: AutoHashMap(usize, Style) = .init(self.ending.container.allocator); 1269 | while (inner_it.next()) |inner| { 1270 | try hm.container.put(inner.key_ptr.*, inner.value_ptr.*); 1271 | } 1272 | try other.ending.container.put(end.key_ptr.*, hm); 1273 | } 1274 | 1275 | return other; 1276 | } 1277 | }; 1278 | const LoopExitCode = enum { 1279 | exit, 1280 | retry, 1281 | }; 1282 | const DeferredAction = union(enum) { 1283 | handle_resize_event: bool, // reset_origin 1284 | try_update_once: u8, // dummy 1285 | }; 1286 | const Callback = struct { 1287 | f: *const fn (*anyopaque) void, 1288 | context: *anyopaque, 1289 | 1290 | pub fn makeHandler(comptime T: type, comptime InnerT: type, comptime name: []const u8) type { 1291 | return struct { 1292 | pub fn theHandler(context: *anyopaque) void { 1293 | const ctx: T = @ptrCast(@alignCast(context)); 1294 | @field(InnerT, name)(ctx); 1295 | } 1296 | }; 1297 | } 1298 | }; 1299 | fn Callback1(comptime T: type) type { 1300 | return struct { 1301 | f: *const fn (*anyopaque, T) void, 1302 | context: *anyopaque, 1303 | 1304 | pub fn makeHandler(comptime U: type, comptime InnerT: type, comptime name: []const u8) type { 1305 | return struct { 1306 | pub fn theHandler(context: *anyopaque, value: T) void { 1307 | const ctx: U = @ptrCast(@alignCast(context)); 1308 | @field(InnerT, name)(ctx, value); 1309 | } 1310 | }; 1311 | } 1312 | }; 1313 | } 1314 | fn CallbackReturning(comptime T: type) type { 1315 | return struct { 1316 | f: *const fn (*anyopaque) T, 1317 | context: *anyopaque, 1318 | 1319 | pub fn makeHandler(comptime U: type, comptime InnerT: type, comptime name: []const u8) type { 1320 | return struct { 1321 | pub fn theHandler(context: *anyopaque) T { 1322 | const ctx: U = @ptrCast(@alignCast(context)); 1323 | return @field(InnerT, name)(ctx); 1324 | } 1325 | }; 1326 | } 1327 | }; 1328 | } 1329 | 1330 | // Error set stored in `input_error` 1331 | const InputError = 1332 | std.Io.Writer.Error || 1333 | std.mem.Allocator.Error || 1334 | std.posix.PipeError || 1335 | std.posix.ReadError || 1336 | error{ Empty, Eof, SystemResource }; 1337 | 1338 | on: struct { 1339 | display_refresh: ?Callback = null, 1340 | paste: ?Callback1([]const u32) = null, 1341 | tab_complete: ?CallbackReturning(GetLineError![]const Module.CompletionSuggestion) = null, 1342 | } = .{}, 1343 | 1344 | allocator: Allocator, 1345 | buffer: ArrayList(u32), 1346 | finished: bool = false, 1347 | search_editor: ?*Self = null, 1348 | is_searching: bool = false, 1349 | reset_buffer_on_search_end: bool = false, 1350 | search_offset: usize = 0, 1351 | search_offset_state: enum { 1352 | unbiased, 1353 | backwards, 1354 | forwards, 1355 | } = .unbiased, 1356 | pre_search_cursor: usize = 0, 1357 | pre_search_buffer: ArrayList(u32), 1358 | pending_chars: ArrayList(u8), 1359 | incomplete_data: ArrayList(u8), 1360 | input_error: ?Wrapped(InputError) = null, // ?Error behaves weirdly - `null` seems to be equal to whatever error number 0 represents, so the null state cannot be represented at all. 1361 | returned_line: []const u8, 1362 | cursor: usize = 0, 1363 | drawn_cursor: usize = 0, 1364 | drawn_end_of_line_offset: usize = 0, 1365 | inline_search_cursor: usize = 0, 1366 | chars_touched_in_the_middle: usize = 0, 1367 | times_tab_pressed: usize = 0, 1368 | num_columns: usize = 0, 1369 | num_lines: usize = 0, 1370 | previous_num_columns: usize = 0, 1371 | extra_forward_lines: usize = 0, 1372 | shown_lines: usize = 0, 1373 | cached_prompt_metrics: StringMetrics, 1374 | old_prompt_metrics: StringMetrics, 1375 | cached_buffer_metrics: StringMetrics, 1376 | prompt_lines_at_suggestion_initiation: usize = 0, 1377 | cached_prompt_valid: bool = false, 1378 | origin_row: usize = 0, 1379 | origin_column: usize = 0, 1380 | has_origin_reset_scheduled: bool = false, 1381 | suggestion_display: SuggestionDisplay, 1382 | remembered_suggestion_static_data: ArrayList(u32), 1383 | new_prompt: ArrayList(u8), 1384 | suggestion_manager: SuggestionManager, 1385 | always_refresh: bool = false, 1386 | tab_direction: enum { 1387 | forward, 1388 | backward, 1389 | } = .forward, 1390 | callback_machine: KeyCallbackMachine, 1391 | termios: termios = undefined, 1392 | default_termios: termios = undefined, 1393 | was_interrupted: bool = false, 1394 | was_resized: bool = false, 1395 | history: ArrayList(HistoryEntry), 1396 | history_cursor: usize = 0, 1397 | history_capacity: usize = 1024, 1398 | history_dirty: bool = false, 1399 | input_state: InputState = .free, 1400 | previous_free_state: InputState = .free, 1401 | current_spans: DrawnSpans, 1402 | drawn_spans: ?DrawnSpans = null, 1403 | paste_buffer: ArrayList(u32), 1404 | initialized: bool = false, 1405 | refresh_needed: bool = false, 1406 | signal_handlers: [2]i32 = .{ 0, 0 }, 1407 | is_editing: bool = false, 1408 | prohibit_input_processing: bool = false, 1409 | have_unprocessed_read_event: bool = false, 1410 | configuration: Configuration, 1411 | event_loop: Loop, 1412 | 1413 | const Loop = if (builtin.os.tag == .wasi or builtin.os.tag == .uefi) 1414 | struct { 1415 | pub fn init(allocator: Allocator, configuration: Configuration) Loop { 1416 | _ = configuration; 1417 | _ = allocator; 1418 | return .{}; 1419 | } 1420 | 1421 | pub fn restart(self: *Loop) !void { 1422 | _ = self; 1423 | } 1424 | 1425 | pub fn stop(self: *Loop) !void { 1426 | _ = self; 1427 | } 1428 | 1429 | pub fn pump(self: *Loop) void { 1430 | _ = self; 1431 | } 1432 | 1433 | pub const Process = struct { 1434 | pub fn done(self: *@This()) void { 1435 | _ = self; 1436 | } 1437 | }; 1438 | 1439 | pub fn process(self: *Loop) !Process { 1440 | _ = self; 1441 | return .{}; 1442 | } 1443 | 1444 | pub fn loopAction(self: *Loop, code: LoopExitCode) !void { 1445 | _ = self; 1446 | _ = code; 1447 | } 1448 | 1449 | pub fn deferredInvoke(self: *Loop, action: DeferredAction) !void { 1450 | _ = self; 1451 | _ = action; 1452 | } 1453 | 1454 | pub fn deinit(self: *Loop) void { 1455 | _ = self; 1456 | } 1457 | 1458 | pub const HandleResult = struct { 1459 | e: ?Wrapped(error{ ZiglineEventLoopExit, ZiglineEventLoopRetry }), 1460 | }; 1461 | 1462 | pub fn handle(self: *Loop, handlers: anytype) !HandleResult { 1463 | _ = self; 1464 | _ = handlers; 1465 | return .{ .e = null }; 1466 | } 1467 | } 1468 | else 1469 | struct { 1470 | enable_signal_handling: bool, 1471 | control_thread: ?Thread = null, 1472 | control_thread_exited: bool = false, 1473 | thread_kill_pipe: ?struct { 1474 | write: std.posix.fd_t, 1475 | read: std.posix.fd_t, 1476 | } = null, 1477 | queue_cond_mutex: Mutex = .{}, 1478 | queue_condition: Condition = .{}, 1479 | logic_cond_mutex: Mutex = .{}, 1480 | logic_condition: Condition = .{}, 1481 | loop_queue: Queue(LoopExitCode), 1482 | deferred_action_queue: Queue(DeferredAction), 1483 | signal_queue: Queue(Signal), 1484 | input_error: ?Wrapped(InputError) = null, 1485 | 1486 | pub fn init(allocator: Allocator, configuration: Configuration) Loop { 1487 | return .{ 1488 | .enable_signal_handling = configuration.enable_signal_handling, 1489 | .loop_queue = .init(allocator), 1490 | .deferred_action_queue = .init(allocator), 1491 | .signal_queue = .init(allocator), 1492 | }; 1493 | } 1494 | 1495 | pub fn restart(self: *Loop) !void { 1496 | try self.stop(); 1497 | self.control_thread_exited = false; 1498 | self.control_thread = try Thread.spawn(.{}, Loop.controlThreadMain, .{self}); 1499 | } 1500 | 1501 | pub fn stop(self: *Loop) !void { 1502 | if (self.control_thread) |t| { 1503 | self.nicelyAskControlThreadToDie() catch {}; 1504 | self.logic_condition.broadcast(); 1505 | t.join(); 1506 | } 1507 | } 1508 | 1509 | pub fn pump(self: *Loop) void { 1510 | self.queue_condition.wait(&self.queue_cond_mutex); 1511 | } 1512 | 1513 | fn nicelyAskControlThreadToDie(self: *Loop) !void { 1514 | if (self.control_thread_exited) { 1515 | return; 1516 | } 1517 | 1518 | // In the absence of way to interrupt threads, we're just gonna write to it and hope it dies on its own pace. 1519 | if (self.thread_kill_pipe) |pipes| { 1520 | _ = std.posix.write(pipes.write, "x") catch 0; 1521 | } 1522 | } 1523 | 1524 | pub fn deinit(self: *Loop) void { 1525 | self.loop_queue.deinit(); 1526 | self.deferred_action_queue.deinit(); 1527 | self.signal_queue.deinit(); 1528 | } 1529 | 1530 | const Process = struct { 1531 | loop: *Loop, 1532 | pub fn done(self: *@This()) void { 1533 | self.loop.queue_cond_mutex.unlock(); 1534 | } 1535 | }; 1536 | 1537 | pub fn process(self: *Loop) !Process { 1538 | self.queue_cond_mutex.lock(); 1539 | 1540 | if (self.thread_kill_pipe == null) { 1541 | const pipe = try SystemCapabilities.pipe(); 1542 | self.thread_kill_pipe = .{ 1543 | .read = pipe[0], 1544 | .write = pipe[1], 1545 | }; 1546 | } 1547 | 1548 | return .{ .loop = self }; 1549 | } 1550 | 1551 | pub fn loopAction(self: *Loop, code: LoopExitCode) !void { 1552 | try self.loop_queue.enqueue(code); 1553 | self.queue_condition.broadcast(); 1554 | } 1555 | 1556 | pub fn deferredInvoke(self: *Loop, action: DeferredAction) !void { 1557 | try self.deferred_action_queue.enqueue(action); 1558 | } 1559 | 1560 | fn controlThreadMain(self: *Loop) void { 1561 | defer self.control_thread_exited = true; 1562 | 1563 | const stdin = SystemCapabilities.stdin(); 1564 | 1565 | std.debug.assert(self.thread_kill_pipe != null); 1566 | 1567 | var pollfds: [3]SystemCapabilities.pollfd = @splat(undefined); 1568 | SystemCapabilities.setPollFd(&pollfds[0], stdin.handle); 1569 | SystemCapabilities.setPollFd(&pollfds[1], self.thread_kill_pipe.?.read); 1570 | pollfds[0].events = SystemCapabilities.POLL_IN; 1571 | pollfds[1].events = SystemCapabilities.POLL_IN; 1572 | pollfds[2].events = 0; 1573 | 1574 | var nfds: SystemCapabilities.nfds_t = 2; 1575 | 1576 | if (self.enable_signal_handling) { 1577 | SystemCapabilities.setPollFd(&pollfds[2], signalHandlingData.?.pipe.read); 1578 | pollfds[2].events = SystemCapabilities.POLL_IN; 1579 | nfds = 3; 1580 | } 1581 | 1582 | defer self.queue_condition.broadcast(); 1583 | 1584 | while (true) { 1585 | self.logic_cond_mutex.lock(); 1586 | 1587 | { 1588 | defer self.logic_cond_mutex.unlock(); 1589 | const rc = SystemCapabilities.poll(&pollfds, nfds, std.math.maxInt(i32)); 1590 | if (rc < 0) { 1591 | self.input_error = switch (std.posix.errno(rc)) { 1592 | .INTR => { 1593 | continue; 1594 | }, 1595 | .NOMEM => toWrapped(InputError, error.SystemResource), 1596 | else => { 1597 | unreachable; 1598 | }, 1599 | }; 1600 | self.loop_queue.enqueue(.exit) catch { 1601 | break; 1602 | }; 1603 | self.queue_condition.broadcast(); 1604 | break; 1605 | } 1606 | } 1607 | 1608 | if (pollfds[1].revents & SystemCapabilities.POLL_IN != 0) { 1609 | // We're supposed to die...after draining the pipe. 1610 | var buf: [8]u8 = @splat(0); 1611 | _ = std.posix.read(self.thread_kill_pipe.?.read, &buf) catch 0; 1612 | break; 1613 | } 1614 | 1615 | if (!is_windows) { 1616 | if (pollfds[2].revents & SystemCapabilities.POLL_IN != 0) no_read: { 1617 | // A signal! Let's handle it. 1618 | var file: std.fs.File = .{ .handle = signalHandlingData.?.pipe.read }; 1619 | var buffer: [4]u8 = undefined; 1620 | var file_reader = file.reader(&buffer); 1621 | const reader = &file_reader.interface; 1622 | const signo = reader.takeInt(i32, .little) catch { 1623 | break :no_read; 1624 | }; 1625 | switch (signo) { 1626 | std.posix.SIG.WINCH => { 1627 | self.signal_queue.enqueue(.SIGWINCH) catch { 1628 | break :no_read; 1629 | }; 1630 | }, 1631 | else => { 1632 | break :no_read; 1633 | }, 1634 | } 1635 | self.queue_condition.broadcast(); 1636 | // Wait for the main thread to process the event, any further input between now and then will be 1637 | // picked up either immediately, or in the next cycle. 1638 | self.logic_cond_mutex.lock(); 1639 | defer self.logic_cond_mutex.unlock(); 1640 | self.logic_condition.wait(&self.logic_cond_mutex); 1641 | } 1642 | } 1643 | 1644 | if (pollfds[0].revents & SystemCapabilities.POLL_IN != 0) { 1645 | self.deferred_action_queue.enqueue(.{ .try_update_once = 0 }) catch {}; 1646 | self.logic_cond_mutex.lock(); 1647 | defer self.logic_cond_mutex.unlock(); 1648 | self.queue_condition.broadcast(); 1649 | // Wait for the main thread to process the event, any further input between now and then will be 1650 | // picked up either immediately, or in the next cycle. 1651 | self.logic_condition.wait(&self.logic_cond_mutex); 1652 | } 1653 | } 1654 | } 1655 | 1656 | pub const HandleResult = struct { 1657 | const Error = error{ ZiglineEventLoopExit, ZiglineEventLoopRetry }; 1658 | e: ?Wrapped(@This().Error), 1659 | }; 1660 | fn handleSignalOrLoopAction(self: *Loop, handlers: anytype) !void { 1661 | while (!self.signal_queue.isEmpty()) { 1662 | const signal = self.signal_queue.dequeue(); 1663 | try handlers.signal.handleEvent(signal); 1664 | } 1665 | 1666 | while (!self.loop_queue.isEmpty()) { 1667 | const code = self.loop_queue.dequeue(); 1668 | switch (code) { 1669 | .exit => { 1670 | return error.ZiglineEventLoopExit; 1671 | }, 1672 | .retry => { 1673 | return error.ZiglineEventLoopRetry; 1674 | }, 1675 | } 1676 | } 1677 | } 1678 | 1679 | pub fn handle(self: *Loop, handlers: anytype) !HandleResult { 1680 | defer self.logic_condition.broadcast(); 1681 | 1682 | while (!self.signal_queue.isEmpty() or !self.loop_queue.isEmpty() or !self.deferred_action_queue.isEmpty()) { 1683 | try self.handleSignalOrLoopAction(handlers); 1684 | 1685 | while (!self.deferred_action_queue.isEmpty()) { 1686 | const action = self.deferred_action_queue.dequeue(); 1687 | try handlers.deferred_action.handleEvent(action); 1688 | try self.handleSignalOrLoopAction(handlers); 1689 | } 1690 | 1691 | if (self.input_error) |e| { 1692 | return fromWrapped(e); 1693 | } 1694 | } 1695 | 1696 | return .{ .e = null }; 1697 | } 1698 | }; 1699 | 1700 | pub fn init(allocator: Allocator, configuration: Configuration) Self { 1701 | return .{ 1702 | .allocator = allocator, 1703 | .buffer = .init(allocator), 1704 | .callback_machine = .init(allocator), 1705 | .pre_search_buffer = .init(allocator), 1706 | .pending_chars = .init(allocator), 1707 | .incomplete_data = .init(allocator), 1708 | .returned_line = &.{}, 1709 | .suggestion_display = .init(allocator), 1710 | .remembered_suggestion_static_data = .init(allocator), 1711 | .history = .init(allocator), 1712 | .current_spans = .init(allocator), 1713 | .new_prompt = .init(allocator), 1714 | .suggestion_manager = .init(allocator), 1715 | .paste_buffer = .init(allocator), 1716 | .configuration = configuration, 1717 | .cached_prompt_metrics = .init(allocator), 1718 | .old_prompt_metrics = .init(allocator), 1719 | .cached_buffer_metrics = .init(allocator), 1720 | .event_loop = .init(allocator, configuration), 1721 | }; 1722 | } 1723 | 1724 | pub fn deinit(self: *Self) void { 1725 | self.event_loop.stop() catch {}; 1726 | self.buffer.deinit(); 1727 | self.pre_search_buffer.deinit(); 1728 | self.pending_chars.deinit(); 1729 | self.incomplete_data.deinit(); 1730 | self.remembered_suggestion_static_data.deinit(); 1731 | self.history.deinit(); 1732 | self.new_prompt.deinit(); 1733 | self.suggestion_manager.deinit(); 1734 | self.suggestion_display.deinit(); 1735 | self.paste_buffer.deinit(); 1736 | self.current_spans.deinit(); 1737 | self.callback_machine.deinit(); 1738 | self.cached_buffer_metrics.deinit(); 1739 | self.cached_prompt_metrics.deinit(); 1740 | self.event_loop.deinit(); 1741 | if (self.drawn_spans) |*spans| { 1742 | spans.deinit(); 1743 | } 1744 | } 1745 | 1746 | pub fn reFetchDefaultTermios(self: *Self) !void { 1747 | const t = try getTermios(); 1748 | self.default_termios = t; 1749 | if (self.configuration.operation_mode == .Full) { 1750 | clearEchoAndICanon(&t); 1751 | } 1752 | self.termios = t; 1753 | } 1754 | 1755 | pub const AddToHistoryError = std.mem.Allocator.Error; 1756 | 1757 | pub fn addToHistory(self: *Self, line: []const u8) !void { 1758 | const entry: HistoryEntry = try .init(self.allocator, line); 1759 | try self.history.container.append(entry); 1760 | } 1761 | 1762 | pub const LoadHistoryError = 1763 | std.mem.Allocator.Error || 1764 | std.fs.File.OpenError || 1765 | std.fs.File.ReadError; 1766 | 1767 | pub fn loadHistory(self: *Self, path: []const u8) LoadHistoryError!void { 1768 | var history_file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) { 1769 | error.FileNotFound => return, 1770 | else => return err, 1771 | }; 1772 | defer history_file.close(); 1773 | 1774 | var buffer: [1024]u8 = undefined; 1775 | var history_file_reader = history_file.reader(&buffer); 1776 | const reader = &history_file_reader.interface; 1777 | 1778 | const data = reader.allocRemaining(self.allocator, .unlimited) catch |err| switch (err) { 1779 | error.OutOfMemory => return error.OutOfMemory, 1780 | error.ReadFailed => return history_file_reader.err.?, 1781 | error.StreamTooLong => unreachable, 1782 | }; 1783 | defer self.allocator.free(data); 1784 | var it = std.mem.splitSequence(u8, data, "\n\n"); 1785 | while (it.next()) |str| { 1786 | if (str.len == 0 or (str.len == 1 and str[0] == '\n')) { 1787 | continue; 1788 | } 1789 | 1790 | const entry_start = std.mem.indexOf(u8, str, "::") orelse 0; 1791 | const timestamp = std.fmt.parseInt(i64, str[0..entry_start], 10) catch 0; 1792 | const entry = try HistoryEntry.initWithTimestamp( 1793 | self.allocator, 1794 | str[(if (entry_start == 0) 0 else entry_start + 2)..], 1795 | timestamp, 1796 | ); 1797 | try self.history.container.append(entry); 1798 | } 1799 | } 1800 | 1801 | pub const SaveHistoryError = 1802 | std.fs.File.OpenError || 1803 | std.fs.File.WriteError; 1804 | 1805 | pub fn saveHistory(self: *Self, path: []const u8) SaveHistoryError!void { 1806 | var history_file = try std.fs.cwd().createFile(path, .{}); 1807 | defer history_file.close(); 1808 | 1809 | var buffer: [1024]u8 = undefined; 1810 | var history_file_writer = history_file.writer(&buffer); 1811 | const writer = &history_file_writer.interface; 1812 | 1813 | for (self.history.container.items) |entry| { 1814 | writer.print("{d}::{s}\n\n", .{ entry.timestamp, entry.entry }) catch { 1815 | return history_file_writer.err.?; 1816 | }; 1817 | } 1818 | writer.flush() catch { 1819 | return history_file_writer.err.?; 1820 | }; 1821 | } 1822 | 1823 | fn addingStyleWouldDamageDrawnSpans(self: *Self, start: usize, end: usize) bool { 1824 | if (self.drawn_spans == null) { 1825 | return false; 1826 | } 1827 | 1828 | var spans_starting = self.drawn_spans.?.starting.container; 1829 | var spans_ending = self.drawn_spans.?.ending.container; 1830 | 1831 | var startIt = spans_starting.iterator(); 1832 | while (startIt.next()) |starting| { 1833 | var innerIt = starting.value_ptr.container.iterator(); 1834 | while (innerIt.next()) |inner| { 1835 | if (inner.key_ptr.* >= start and inner.key_ptr.* <= end) { 1836 | return true; 1837 | } 1838 | } 1839 | } 1840 | 1841 | var endIt = spans_ending.iterator(); 1842 | while (endIt.next()) |ending| { 1843 | var innerIt = ending.value_ptr.container.iterator(); 1844 | while (innerIt.next()) |inner| { 1845 | if (inner.key_ptr.* >= start and inner.key_ptr.* <= end) { 1846 | return true; 1847 | } 1848 | } 1849 | } 1850 | 1851 | return false; 1852 | } 1853 | 1854 | pub fn stylize(self: *Self, span: Span, style: Style) !void { 1855 | if (span.isEmpty()) { 1856 | return; 1857 | } 1858 | 1859 | var start = span.begin; 1860 | var end = span.end; 1861 | 1862 | if (span.mode == .byte_oriented) { 1863 | const offsets = self.byteOffsetRangeToCodePointOffsetRange(span.begin, span.end, 0, false); 1864 | start = offsets.start; 1865 | end = offsets.end; 1866 | } 1867 | 1868 | var spans_starting = &self.current_spans.starting.container; 1869 | var spans_ending = &self.current_spans.ending.container; 1870 | 1871 | var put_result = try spans_starting.getOrPut(start); 1872 | var starting_map = put_result.value_ptr; 1873 | if (!put_result.found_existing) { 1874 | starting_map.* = .init(self.allocator); 1875 | } 1876 | 1877 | try starting_map.container.put(end, style); 1878 | 1879 | put_result = try spans_ending.getOrPut(end); 1880 | var ending_map = put_result.value_ptr; 1881 | if (!put_result.found_existing) { 1882 | ending_map.* = .init(self.allocator); 1883 | } 1884 | 1885 | try ending_map.container.put(start, style); 1886 | 1887 | if (self.addingStyleWouldDamageDrawnSpans(start, end)) { 1888 | self.refresh_needed = true; 1889 | } 1890 | } 1891 | 1892 | pub fn stripStyles(self: *Self) void { 1893 | self.current_spans.deinit(); 1894 | self.current_spans = .init(self.allocator); 1895 | } 1896 | 1897 | fn getLineNonInteractive(self: *Self, prompt: []const u8) ![]const u8 { 1898 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 1899 | var stderr = &stderr_writer.interface; 1900 | try stderr.writeAll(prompt); 1901 | 1902 | var stdin_buffer: [1024]u8 = undefined; 1903 | var stdin_reader = SystemCapabilities.stdin().reader(&stdin_buffer); 1904 | const stdin = &stdin_reader.interface; 1905 | 1906 | var allocating_writer: std.Io.Writer.Allocating = .init(self.allocator); 1907 | defer allocating_writer.deinit(); 1908 | const writer = &allocating_writer.writer; 1909 | 1910 | streamUntilEol(stdin, writer) catch |e| switch (e) { 1911 | error.EndOfStream => return error.Eof, 1912 | else => return e, 1913 | }; 1914 | 1915 | return allocating_writer.toOwnedSlice(); 1916 | } 1917 | 1918 | fn streamUntilEol(reader: *std.Io.Reader, writer: *std.Io.Writer) !void { 1919 | if (!is_windows) { 1920 | // eol is '\n', so we can just use streamDelimiter. 1921 | _ = try reader.streamDelimiter(writer, '\n'); 1922 | return; 1923 | } 1924 | 1925 | // Read until '\r', return if '\n' follows, otherwise keep reading. 1926 | while (true) { 1927 | _ = try reader.streamDelimiter(writer, '\r'); 1928 | const bytes = try reader.peekArray(2); 1929 | std.debug.assert(bytes[0] == '\r'); 1930 | if (bytes[1] == '\n') return; 1931 | } 1932 | } 1933 | 1934 | pub const GetLineError = 1935 | InputError || 1936 | std.Io.Reader.Error || 1937 | std.Io.Writer.Error || 1938 | std.mem.Allocator.Error || 1939 | std.posix.PipeError || 1940 | std.posix.TermiosGetError || 1941 | std.posix.TermiosSetError || 1942 | std.Thread.SpawnError || 1943 | error{ CodepointTooLarge, Utf8CannotEncodeSurrogateHalf }; 1944 | 1945 | pub fn getLine(self: *Self, prompt: []const u8) GetLineError![]const u8 { 1946 | if (self.configuration.operation_mode == .non_interactive) { 1947 | // Disable all the fancy stuff, use a plain read. 1948 | return self.getLineNonInteractive(prompt); 1949 | } 1950 | 1951 | var loop_handle = try self.event_loop.process(); 1952 | defer loop_handle.done(); 1953 | 1954 | if (should_enable_signal_handling) { 1955 | if (self.configuration.enable_signal_handling) { 1956 | if (signalHandlingData == null) { 1957 | const pipe = try SystemCapabilities.pipe(); 1958 | signalHandlingData = .{ 1959 | .pipe = .{ 1960 | .read = pipe[0], 1961 | .write = pipe[1], 1962 | }, 1963 | }; 1964 | 1965 | signalHandlingData.?.old_sigint = @as(SystemCapabilities.Sigaction, undefined); 1966 | signalHandlingData.?.old_sigwinch = @as(SystemCapabilities.Sigaction, undefined); 1967 | var act: std.posix.Sigaction = .{ 1968 | .handler = .{ .handler = @TypeOf(signalHandlingData.?).handleSignal }, 1969 | .mask = std.posix.sigemptyset(), 1970 | .flags = 0, 1971 | }; 1972 | std.posix.sigaction(std.posix.SIG.INT, &act, &signalHandlingData.?.old_sigint.?); 1973 | std.posix.sigaction(std.posix.SIG.WINCH, &act, &signalHandlingData.?.old_sigwinch.?); 1974 | } 1975 | } 1976 | } 1977 | 1978 | start: while (true) { 1979 | try self.initialize(); 1980 | 1981 | self.is_editing = true; 1982 | const old_cols = self.num_columns; 1983 | const old_lines = self.num_lines; 1984 | self.getTerminalSize(); 1985 | 1986 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 1987 | const stderr = &stderr_writer.interface; 1988 | 1989 | if (self.configuration.enable_bracketed_paste) { 1990 | try stderr.writeAll("\x1b[?2004h"); 1991 | } 1992 | 1993 | if (self.num_columns != old_cols or self.num_lines != old_lines) { 1994 | self.refresh_needed = true; 1995 | } 1996 | 1997 | try self.setPrompt(prompt); 1998 | try self.reset(); 1999 | self.stripStyles(); 2000 | 2001 | const prompt_lines = @max(self.currentPromptMetrics().line_metrics.container.items.len, 1) - 1; 2002 | for (0..prompt_lines) |_| { 2003 | try stderr.writeAll("\n"); 2004 | } 2005 | 2006 | try vtMoveRelative(-@as(i64, @intCast(prompt_lines)), 0); 2007 | _ = self.setOrigin(true); 2008 | 2009 | self.history_cursor = self.history.container.items.len; 2010 | 2011 | try self.refreshDisplay(); 2012 | 2013 | try self.event_loop.restart(); 2014 | 2015 | var had_incomplete_data_at_start = false; 2016 | if (self.incomplete_data.container.items.len != 0) { 2017 | try self.event_loop.deferredInvoke(.{ .try_update_once = 0 }); 2018 | had_incomplete_data_at_start = true; 2019 | } 2020 | 2021 | // FIXME: Install signal handlers. 2022 | 2023 | while (true) { 2024 | if (!had_incomplete_data_at_start) { 2025 | self.event_loop.pump(); 2026 | } 2027 | had_incomplete_data_at_start = false; 2028 | 2029 | const result: Loop.HandleResult = self.event_loop.handle(.{ 2030 | .signal = struct { 2031 | editor: *Self, 2032 | 2033 | pub fn handleEvent(s: @This(), event: Signal) !void { 2034 | switch (event) { 2035 | .SIGWINCH => { 2036 | try s.editor.resized(); 2037 | }, 2038 | } 2039 | } 2040 | }{ .editor = self }, 2041 | 2042 | .deferred_action = struct { 2043 | editor: *Self, 2044 | pub fn handleEvent(s: @This(), action: DeferredAction) !void { 2045 | switch (action) { 2046 | .handle_resize_event => |handle_resize_event| { 2047 | try s.editor.handleResizeEvent(handle_resize_event); 2048 | }, 2049 | .try_update_once => { 2050 | try s.editor.tryUpdateOnce(); 2051 | }, 2052 | } 2053 | } 2054 | }{ .editor = self }, 2055 | }) catch |e| switch (e) { 2056 | error.ZiglineEventLoopExit, error.ZiglineEventLoopRetry => .{ .e = toWrapped(Loop.HandleResult.Error, e) }, 2057 | else => b: { 2058 | self.input_error = toWrapped(InputError, e); 2059 | break :b .{ .e = null }; 2060 | }, 2061 | }; 2062 | 2063 | if (result.e) |err| { 2064 | switch (fromWrapped(err)) { 2065 | error.ZiglineEventLoopExit => { 2066 | self.finished = false; 2067 | if (self.input_error) |e| { 2068 | return fromWrapped(e); 2069 | } 2070 | return self.returned_line; 2071 | }, 2072 | error.ZiglineEventLoopRetry => { 2073 | continue :start; 2074 | }, 2075 | } 2076 | } 2077 | } 2078 | } 2079 | } 2080 | 2081 | fn initialize(self: *Self) !void { 2082 | if (self.initialized) { 2083 | return; 2084 | } 2085 | 2086 | var t = try getTermios(); 2087 | self.default_termios = t; 2088 | 2089 | self.getTerminalSize(); 2090 | 2091 | if (self.configuration.operation_mode == .full) { 2092 | clearEchoAndICanon(&t); 2093 | } 2094 | 2095 | try setTermios(t); 2096 | self.termios = t; 2097 | try self.setDefaultKeybinds(); 2098 | self.initialized = true; 2099 | } 2100 | 2101 | fn interrupted(self: *Self) !bool { 2102 | if (self.is_searching) { 2103 | return self.search_editor.?.interrupted(); 2104 | } 2105 | 2106 | if (!self.is_editing) { 2107 | return false; 2108 | } 2109 | 2110 | { 2111 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 2112 | const stderr = &stderr_writer.interface; 2113 | try stderr.writeAll("^C"); 2114 | } 2115 | 2116 | self.buffer.container.clearAndFree(); 2117 | self.chars_touched_in_the_middle = 0; 2118 | self.cursor = 0; 2119 | 2120 | { 2121 | var stderr_buffer: [1024]u8 = undefined; 2122 | var stderr_writer = SystemCapabilities.stderr().writer(&stderr_buffer); 2123 | const stderr = &stderr_writer.interface; 2124 | 2125 | try self.repositionCursor(stderr, true); 2126 | // FIXME: Suggestion display cleanup. 2127 | try stderr.writeAll("\n"); 2128 | try stderr.flush(); 2129 | } 2130 | 2131 | self.was_interrupted = true; 2132 | 2133 | self.buffer.container.clearAndFree(); 2134 | self.chars_touched_in_the_middle = 0; 2135 | self.is_editing = false; 2136 | try self.restore(); 2137 | try self.event_loop.loopAction(.retry); 2138 | 2139 | return false; 2140 | } 2141 | 2142 | fn resized(self: *Self) !void { 2143 | self.was_resized = true; 2144 | self.previous_num_columns = self.num_columns; 2145 | self.getTerminalSize(); 2146 | 2147 | if (!self.has_origin_reset_scheduled) { 2148 | // Reset the origin, but make sure it doesn't blow up if we fail to read it. 2149 | if (self.setOrigin(false)) { 2150 | try self.handleResizeEvent(false); 2151 | } else { 2152 | try self.event_loop.deferredInvoke(.{ .handle_resize_event = true }); 2153 | self.has_origin_reset_scheduled = true; 2154 | } 2155 | } 2156 | } 2157 | 2158 | pub fn getCursor(self: *Self) usize { 2159 | return self.cursor; 2160 | } 2161 | 2162 | pub fn setCursor(self: *Self, cursor: usize) void { 2163 | if (cursor > self.buffer.container.items.len) { 2164 | self.cursor = self.buffer.container.items.len; 2165 | } else { 2166 | self.cursor = cursor; 2167 | } 2168 | } 2169 | 2170 | pub fn getBuffer(self: *Self) []const u32 { 2171 | return self.buffer.container.items; 2172 | } 2173 | 2174 | pub fn getBufferedLine(self: *Self) ![]const u8 { 2175 | return self.getBufferedLineUpTo(self.getBuffer().len); 2176 | } 2177 | 2178 | pub fn getBufferedLineUpTo(self: *Self, index: usize) ![]const u8 { 2179 | var u8buffer: std.array_list.Managed(u8) = .init(self.allocator); 2180 | defer u8buffer.deinit(); 2181 | 2182 | for (self.buffer.container.items[0..index]) |code_point| { 2183 | var u8buf: [4]u8 = @splat(0); 2184 | const length = try std.unicode.utf8Encode(@intCast(code_point), &u8buf); 2185 | try u8buffer.appendSlice(u8buf[0..length]); 2186 | } 2187 | 2188 | return u8buffer.toOwnedSlice(); 2189 | } 2190 | 2191 | pub fn setPrompt(self: *Self, prompt: []const u8) !void { 2192 | if (self.cached_prompt_valid) { 2193 | self.old_prompt_metrics = self.cached_prompt_metrics; 2194 | } 2195 | self.cached_prompt_valid = false; 2196 | self.cached_prompt_metrics.deinit(); 2197 | self.cached_prompt_metrics = try self.actualRenderedStringMetrics(prompt); 2198 | self.new_prompt.container.clearRetainingCapacity(); 2199 | try self.new_prompt.container.appendSlice(prompt); 2200 | } 2201 | 2202 | fn actualRenderedStringLengthStep( 2203 | metrics: *StringMetrics, 2204 | current_line: *StringMetrics.LineMetrics, 2205 | c: u32, 2206 | next_c: u32, 2207 | state: VTState, 2208 | ) !VTState { 2209 | switch (state) { 2210 | .free => { 2211 | if (c == 0x1b) { 2212 | return .escape; 2213 | } 2214 | if (c == '\r') { 2215 | current_line.length = 0; 2216 | if (metrics.line_metrics.size() != 0) { 2217 | metrics.line_metrics.container.items[metrics.line_metrics.size() - 1] = .{}; 2218 | } 2219 | return state; 2220 | } 2221 | if (c == '\n') { 2222 | try metrics.line_metrics.container.append(current_line.*); 2223 | current_line.length = 0; 2224 | return state; 2225 | } 2226 | current_line.length += 1; 2227 | metrics.total_length += 1; 2228 | return state; 2229 | }, 2230 | .escape => { 2231 | if (c == ']') { 2232 | if (next_c == '0') { 2233 | return .title; 2234 | } 2235 | return state; 2236 | } 2237 | if (c == '[') { 2238 | return .bracket; 2239 | } 2240 | return state; 2241 | }, 2242 | .bracket => { 2243 | if (c >= '0' and c <= '9') { 2244 | return .bracket_args_semi; 2245 | } 2246 | return state; 2247 | }, 2248 | .bracket_args_semi => { 2249 | if (c == ';') { 2250 | return .bracket; 2251 | } 2252 | if (c >= '0' and c <= '9') { 2253 | return state; 2254 | } 2255 | return .free; 2256 | }, 2257 | .title => { 2258 | if (c == 7) { 2259 | return .free; 2260 | } 2261 | return state; 2262 | }, 2263 | } 2264 | } 2265 | 2266 | pub fn actualRenderedStringMetrics(self: *Self, string: []const u8) !StringMetrics { 2267 | var metrics: StringMetrics = .init(self.allocator); 2268 | var current_line: StringMetrics.LineMetrics = .{}; 2269 | 2270 | var state: VTState = .free; 2271 | 2272 | for (0..string.len) |i| { 2273 | const c = string[i]; 2274 | var next_c: u32 = 0; 2275 | if (i + 1 < string.len) { 2276 | next_c = string[i + 1]; 2277 | } 2278 | state = try actualRenderedStringLengthStep(&metrics, ¤t_line, c, next_c, state); 2279 | } 2280 | 2281 | try metrics.line_metrics.container.append(current_line); 2282 | for (metrics.line_metrics.container.items) |line_metric| { 2283 | metrics.max_line_length = @max(line_metric.totalLength(), metrics.max_line_length); 2284 | } 2285 | 2286 | return metrics; 2287 | } 2288 | 2289 | pub fn actualRenderedUnicodeStringMetrics(self: *Self, string: []const u32) !StringMetrics { 2290 | var metrics: StringMetrics = .init(self.allocator); 2291 | var current_line: StringMetrics.LineMetrics = .{}; 2292 | 2293 | var state: VTState = .free; 2294 | 2295 | for (0..string.len) |i| { 2296 | const c = string[i]; 2297 | var next_c: u32 = 0; 2298 | if (i + 1 < string.len) { 2299 | next_c = string[i + 1]; 2300 | } 2301 | state = try actualRenderedStringLengthStep(&metrics, ¤t_line, c, next_c, state); 2302 | } 2303 | 2304 | try metrics.line_metrics.container.append(current_line); 2305 | for (metrics.line_metrics.container.items) |line_metric| { 2306 | metrics.max_line_length = @max(line_metric.totalLength(), metrics.max_line_length); 2307 | } 2308 | 2309 | return metrics; 2310 | } 2311 | 2312 | pub fn clearLine(self: *Self) void { 2313 | _ = self; 2314 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 2315 | const stderr = &stderr_writer.interface; 2316 | stderr.writeAll("\r\x1b[K") catch {}; 2317 | } 2318 | 2319 | pub fn insertString(self: *Self, string: []const u8) void { 2320 | for (string) |code_point| { 2321 | self.insertCodePoint(code_point); 2322 | } 2323 | } 2324 | 2325 | pub fn insertCodePoint(self: *Self, code_point: u32) void { 2326 | var buf: [4]u8 = @splat(0); 2327 | const length = std.unicode.utf8Encode(@intCast(code_point), &buf) catch { 2328 | return; 2329 | }; 2330 | self.pending_chars.container.appendSlice(buf[0..length]) catch { 2331 | return; 2332 | }; 2333 | 2334 | if (self.cursor >= self.buffer.size()) { 2335 | self.buffer.container.append(code_point) catch unreachable; 2336 | self.cursor = self.buffer.size(); 2337 | self.inline_search_cursor = self.cursor; 2338 | return; 2339 | } 2340 | 2341 | self.buffer.container.insert(self.cursor, code_point) catch unreachable; 2342 | self.chars_touched_in_the_middle += 1; 2343 | self.cursor += 1; 2344 | self.inline_search_cursor = self.cursor; 2345 | } 2346 | 2347 | pub fn insertUtf32(self: *Self, utf32: []const u32) void { 2348 | for (utf32) |code_point| { 2349 | self.insertCodePoint(code_point); 2350 | } 2351 | } 2352 | 2353 | pub fn finish(self: *Self) bool { 2354 | self.finished = true; 2355 | return false; 2356 | } 2357 | 2358 | pub fn isEditing(self: *Self) void { 2359 | return self.is_editing; 2360 | } 2361 | 2362 | pub fn prohibitInput(self: *Self) void { 2363 | self.prohibit_input_processing = true; 2364 | } 2365 | 2366 | pub fn allowInput(self: *Self) void { 2367 | self.prohibit_input_processing = false; 2368 | } 2369 | 2370 | fn eatErrors(comptime f: fn (*Self) anyerror!bool) fn (*Self) bool { 2371 | return struct { 2372 | pub fn handler(self: *Self) bool { 2373 | return f(self) catch false; 2374 | } 2375 | }.handler; 2376 | } 2377 | 2378 | fn setDefaultKeybinds(self: *Self) !void { 2379 | try self.registerCharInputCallback('\n', &finish); 2380 | try self.registerCharInputCallback(ctrl('C'), &eatErrors(interrupted)); 2381 | 2382 | // ^N searchForwards, ^P searchBackwards 2383 | try self.registerCharInputCallback(ctrl('N'), &searchForwards); 2384 | try self.registerCharInputCallback(ctrl('P'), &searchBackwards); 2385 | // ^A goHome, ^B cursorLeftCharacter 2386 | try self.registerCharInputCallback(ctrl('A'), &goHome); 2387 | try self.registerCharInputCallback(ctrl('B'), &cursorLeftCharacter); 2388 | // ^D eraseCharacterForwards, ^E goEnd 2389 | try self.registerCharInputCallback(ctrl('D'), &eraseCharacterForwards); 2390 | try self.registerCharInputCallback(ctrl('E'), &goEnd); 2391 | // ^F cursorRightCharacter, ^H eraseCharacterBackwards 2392 | try self.registerCharInputCallback(ctrl('F'), &cursorRightCharacter); 2393 | try self.registerCharInputCallback(ctrl('H'), &eraseCharacterBackwards); 2394 | // DEL, some terminals send this instead of ctrl('H') 2395 | try self.registerCharInputCallback(127, &eraseCharacterBackwards); 2396 | // ^K eraseToEnd, ^L clearScreen, ^R enterSearch 2397 | try self.registerCharInputCallback(ctrl('K'), &eraseToEnd); 2398 | try self.registerCharInputCallback(ctrl('L'), &clearScreen); 2399 | // FIXME: try self.registerCharInputCallback(ctrl('R'), &enterSearch); 2400 | // ^T transposeCharacters 2401 | try self.registerCharInputCallback(ctrl('T'), &transposeCharacters); 2402 | 2403 | // ^[b cursorLeftWord, ^[f cursorRightWord 2404 | try self.registerKeyInputCallback(.{ .code_point = 'b', .modifiers = .alt }, &cursorLeftWord); 2405 | try self.registerKeyInputCallback(.{ .code_point = 'f', .modifiers = .alt }, &cursorRightWord); 2406 | // ^[^B cursorLeftNonspaceWord, ^[^F cursorRightNonspaceWord 2407 | try self.registerKeyInputCallback(.{ .code_point = ctrl('B'), .modifiers = .alt }, &cursorLeftNonspaceWord); 2408 | try self.registerKeyInputCallback(.{ .code_point = ctrl('F'), .modifiers = .alt }, &cursorRightNonspaceWord); 2409 | // ^[^H eraseAlnumWordBackwards 2410 | try self.registerKeyInputCallback(.{ .code_point = ctrl('H'), .modifiers = .alt }, &eraseAlnumWordBackwards); 2411 | // ^[d eraseAlnumWordForwards 2412 | try self.registerKeyInputCallback(.{ .code_point = 'd', .modifiers = .alt }, &eraseAlnumWordForwards); 2413 | // ^[c capitalizeWord, ^[l lowercaseWord, ^[u uppercaseWord, ^[t transposeWords 2414 | try self.registerKeyInputCallback(.{ .code_point = 'c', .modifiers = .alt }, &capitalizeWord); 2415 | try self.registerKeyInputCallback(.{ .code_point = 'l', .modifiers = .alt }, &lowercaseWord); 2416 | try self.registerKeyInputCallback(.{ .code_point = 'u', .modifiers = .alt }, &uppercaseWord); 2417 | // FIXME: try self.registerKeyInputCallback(.{ .code_point = 't', .modifiers = .alt }, &transposeWords); 2418 | 2419 | // Normally ^W eraseWordBackwards 2420 | // Normally ^U killLine 2421 | try self.registerCharInputCallback(ctrl('W'), &eraseWordBackwards); 2422 | try self.registerCharInputCallback(getTermiosCC(self.termios, V.KILL), &killLine); 2423 | try self.registerCharInputCallback(getTermiosCC(self.termios, V.ERASE), &eraseCharacterBackwards); 2424 | } 2425 | 2426 | fn registerKeyInputCallback(self: *Self, key: Key, c: *const fn (*Editor) bool) !void { 2427 | try self.callback_machine.registerKeyInputCallback(&.{key}, c); 2428 | } 2429 | 2430 | fn registerKeySequenceInputCallback(self: *Self, seq: []const Key, c: *const fn (*Editor) bool) !void { 2431 | try self.callback_machine.registerKeyInputCallback(seq, c); 2432 | } 2433 | 2434 | fn registerCharInputCallback(self: *Self, c: u32, f: *const fn (*Editor) bool) !void { 2435 | try self.callback_machine.registerKeyInputCallback(&.{.{ .code_point = c }}, f); 2436 | } 2437 | 2438 | fn vtApplyStyle(style: Style, writer: *std.Io.Writer, is_starting: bool) !void { 2439 | var buffer: [128]u8 = undefined; 2440 | var fba = std.heap.FixedBufferAllocator.init(&buffer); 2441 | 2442 | if (is_starting) { 2443 | var allocator = fba.allocator(); 2444 | const bg = try style.background.toVTEscape(allocator, .background); 2445 | defer allocator.free(bg); 2446 | const fg = try style.foreground.toVTEscape(allocator, .foreground); 2447 | defer allocator.free(fg); 2448 | 2449 | try writer.print("\x1b[{d};{d};{d}m{s}{s}", .{ 2450 | @as(u8, if (style.bold) 1 else 22), 2451 | @as(u8, if (style.underline) 4 else 24), 2452 | @as(u8, if (style.italic) 3 else 23), 2453 | bg, 2454 | fg, 2455 | }); 2456 | } 2457 | } 2458 | 2459 | fn vtMoveAbsolute(self: *Self, row: usize, col: usize, writer: *std.Io.Writer) !void { 2460 | _ = self; 2461 | _ = try writer.print("\x1b[{d};{d}H", .{ row, col }); 2462 | } 2463 | 2464 | fn vtClearToEndOfLine(self: *Self, writer: *std.Io.Writer) !void { 2465 | _ = self; 2466 | _ = try writer.writeAll("\x1b[K"); 2467 | } 2468 | 2469 | fn vtClearLines(self: *Self, above: usize, below: usize, writer: *std.Io.Writer) !void { 2470 | if (above + below == 0) { 2471 | return self.clearLine(); 2472 | } 2473 | 2474 | // Go down below lines... 2475 | if (below > 0) { 2476 | try writer.print("\x1b[{d}B", .{below}); 2477 | } 2478 | 2479 | // ...and clear lines going up. 2480 | for (0..above + below) |i| { 2481 | try writer.print("\x1b[2K", .{}); 2482 | if (i != 1) { 2483 | try writer.print("\x1b[A", .{}); 2484 | } 2485 | } 2486 | 2487 | // Go back down. 2488 | if (above > 0) { 2489 | try writer.print("\x1b[{d}B", .{above}); 2490 | } 2491 | } 2492 | 2493 | fn tryUpdateOnce(self: *Self) !void { 2494 | try self.handleReadEvent(); 2495 | 2496 | if (self.always_refresh) { 2497 | self.refresh_needed = true; 2498 | } 2499 | 2500 | try self.refreshDisplay(); 2501 | 2502 | if (self.drawn_spans) |*spans| { 2503 | spans.deinit(); 2504 | } 2505 | self.drawn_spans = try self.current_spans.deepCopy(); 2506 | 2507 | if (self.finished) { 2508 | try self.reallyQuitEventLoop(); 2509 | } 2510 | } 2511 | 2512 | fn handleReadEvent(self: *Self) !void { 2513 | if (self.prohibit_input_processing) { 2514 | self.have_unprocessed_read_event = true; 2515 | return; 2516 | } 2517 | 2518 | self.prohibit_input_processing = true; 2519 | defer self.prohibit_input_processing = false; 2520 | 2521 | var keybuf: [32]u8 = @splat(0); 2522 | 2523 | var stdin = SystemCapabilities.stdin(); 2524 | 2525 | if (self.incomplete_data.container.items.len == 0) { 2526 | const nread = stdin.read(&keybuf) catch |err| { 2527 | // Zig eats EINTR, so we'll have to delay resize handling until the next read. 2528 | self.finished = true; 2529 | self.input_error = toWrapped(InputError, err); 2530 | return; 2531 | }; 2532 | 2533 | if (nread == 0) { 2534 | self.input_error = toWrapped(InputError, error.Empty); 2535 | self.finished = true; 2536 | return; 2537 | } 2538 | 2539 | try self.incomplete_data.container.appendSlice(keybuf[0..nread]); 2540 | } 2541 | 2542 | var available_bytes = self.incomplete_data.container.items.len; 2543 | 2544 | var reverse_tab = false; 2545 | 2546 | // Discard starting bytes until they make sense as utf-8. 2547 | var valid_bytes: usize = 0; 2548 | while (available_bytes > 0) { 2549 | valid_bytes = utf8ValidRange(self.incomplete_data.container.items[0..available_bytes]); 2550 | if (valid_bytes > 0) { 2551 | break; 2552 | } 2553 | _ = self.incomplete_data.container.orderedRemove(0); 2554 | available_bytes -= 1; 2555 | } 2556 | 2557 | var input_view = std.unicode.Utf8View.initUnchecked(self.incomplete_data.container.items[0..valid_bytes]); 2558 | var consumed_code_points: usize = 0; 2559 | 2560 | // FIXME: These are leaked, we have no way to free them. 2561 | const csi = struct { 2562 | var parameter_bytes: std.array_list.Managed(u8) = undefined; 2563 | var intermediate_bytes: std.array_list.Managed(u8) = undefined; 2564 | var initialized = false; 2565 | }; 2566 | 2567 | if (!csi.initialized) { 2568 | csi.intermediate_bytes = .init(self.allocator); 2569 | csi.parameter_bytes = .init(self.allocator); 2570 | csi.initialized = true; 2571 | } 2572 | 2573 | var csi_parameters: std.array_list.Managed(u32) = .init(self.allocator); 2574 | defer csi_parameters.deinit(); 2575 | 2576 | var csi_final: u8 = 0; 2577 | 2578 | var input_it = input_view.iterator(); 2579 | while (input_it.nextCodepoint()) |code_point| { 2580 | if (self.finished) { 2581 | break; 2582 | } 2583 | 2584 | consumed_code_points += 1; 2585 | if (code_point == 0) { 2586 | continue; 2587 | } 2588 | 2589 | state: switch (self.input_state) { 2590 | .got_escape => switch (code_point) { 2591 | '[' => { 2592 | self.input_state = .csi_expect_parameter; 2593 | continue; 2594 | }, 2595 | else => { 2596 | try self.callback_machine.keyPressed(self, .{ .code_point = code_point, .modifiers = .alt }); 2597 | self.input_state = .free; 2598 | try self.cleanupSuggestions(); 2599 | continue; 2600 | }, 2601 | }, 2602 | .csi_expect_parameter, .csi_expect_intermediate, .csi_expect_final => { 2603 | if (self.input_state == .csi_expect_parameter) { 2604 | if (code_point >= 0x30 and code_point <= 0x3f) { // '0123456789:;<=>?' 2605 | try csi.parameter_bytes.append(@intCast(code_point)); 2606 | continue; 2607 | } 2608 | self.input_state = .csi_expect_intermediate; 2609 | } 2610 | if (self.input_state == .csi_expect_intermediate) { 2611 | if (code_point >= 0x20 and code_point <= 0x2f) { // ' !"#$%&\'()*+,-./' 2612 | try csi.intermediate_bytes.append(@intCast(code_point)); 2613 | continue; 2614 | } 2615 | self.input_state = .csi_expect_final; 2616 | } 2617 | if (self.input_state == .csi_expect_final) { 2618 | self.input_state = self.previous_free_state; 2619 | const is_in_paste = self.input_state == .paste; 2620 | var it = std.mem.splitScalar(u8, csi.parameter_bytes.items, ';'); 2621 | while (it.next()) |parameter| { 2622 | if (std.fmt.parseInt(u8, parameter, 10) catch null) |value| { 2623 | try csi_parameters.append(value); 2624 | } else { 2625 | try csi_parameters.append(0); 2626 | } 2627 | } 2628 | var param1: u32 = 0; 2629 | var param2: u32 = 0; 2630 | if (csi_parameters.items.len >= 1) { 2631 | param1 = csi_parameters.items[0]; 2632 | } 2633 | if (csi_parameters.items.len >= 2) { 2634 | param2 = csi_parameters.items[1]; 2635 | } 2636 | 2637 | var modifiers: CSIMod = .none; 2638 | if (param2 != 0) { 2639 | modifiers = @enumFromInt(@as(u8, @intCast(param2 - 1))); 2640 | } 2641 | 2642 | if (is_in_paste and code_point != '~' and param1 != 201) { 2643 | // The only valid escape to process in paste mode is the stop-paste sequence. 2644 | // so treat everything else as part of the pasted data. 2645 | self.insertCodePoint(0x1b); 2646 | self.insertCodePoint('['); 2647 | self.insertString(csi.parameter_bytes.items); 2648 | self.insertString(csi.intermediate_bytes.items); 2649 | self.insertCodePoint(code_point); 2650 | continue; 2651 | } 2652 | if (!(code_point >= 0x40 and code_point <= 0x7f)) { 2653 | logger.debug("Invalid CSI: {x:02} ({c})", .{ code_point, @as(u8, @intCast(code_point)) }); 2654 | continue; 2655 | } 2656 | 2657 | csi_final = @intCast(code_point); 2658 | csi_parameters.clearAndFree(); 2659 | csi.parameter_bytes.clearAndFree(); 2660 | csi.intermediate_bytes.clearAndFree(); 2661 | csi.initialized = false; 2662 | csi.parameter_bytes.deinit(); 2663 | csi.intermediate_bytes.deinit(); 2664 | 2665 | if (csi_final == 'Z') { 2666 | // 'reverse tab' 2667 | reverse_tab = true; 2668 | break :state; 2669 | } 2670 | 2671 | try self.cleanupSuggestions(); 2672 | 2673 | switch (csi_final) { 2674 | 'A' => { // ^[[A: arrow up 2675 | _ = self.searchBackwards(); 2676 | continue; 2677 | }, 2678 | 'B' => { // ^[[B: arrow down 2679 | _ = self.searchForwards(); 2680 | continue; 2681 | }, 2682 | 'D' => { // ^[[D: arrow left 2683 | if (modifiers == .alt or modifiers == .ctrl) { 2684 | _ = self.cursorLeftWord(); 2685 | } else { 2686 | _ = self.cursorLeftCharacter(); 2687 | } 2688 | continue; 2689 | }, 2690 | 'C' => { // ^[[C: arrow right 2691 | if (modifiers == .alt or modifiers == .ctrl) { 2692 | _ = self.cursorRightWord(); 2693 | } else { 2694 | _ = self.cursorRightCharacter(); 2695 | } 2696 | continue; 2697 | }, 2698 | 'H' => { // ^[[H: home 2699 | // TODO: self.goHome(); 2700 | continue; 2701 | }, 2702 | 'F' => { // ^[[F: end 2703 | // TODO: self.goEnd(); 2704 | continue; 2705 | }, 2706 | 127 => { 2707 | if (modifiers == .ctrl) { 2708 | // TODO: self.eraseAlnumWordBackwards(); 2709 | } else { 2710 | _ = self.eraseCharacterBackwards(); 2711 | } 2712 | continue; 2713 | }, 2714 | '~' => { 2715 | if (param1 == 3) { // ^[[3~: delete 2716 | if (modifiers == .ctrl) { 2717 | // TODO: self.eraseAlnumWordForwards(); 2718 | } else { 2719 | _ = self.eraseCharacterForwards(); 2720 | } 2721 | self.search_offset = 0; 2722 | continue; 2723 | } 2724 | if (self.configuration.enable_bracketed_paste) { 2725 | // ^[[200~: start bracketed paste 2726 | // ^[[201~: end bracketed paste 2727 | if (!is_in_paste and param1 == 200) { 2728 | self.input_state = .paste; 2729 | continue; 2730 | } 2731 | if (is_in_paste and param1 == 201) { 2732 | self.input_state = .free; 2733 | if (self.on.paste) |*cb| { 2734 | cb.f(cb.context, self.paste_buffer.container.items); 2735 | self.paste_buffer.container.clearRetainingCapacity(); 2736 | } 2737 | if (self.paste_buffer.size() > 0) { 2738 | self.insertUtf32(self.paste_buffer.container.items); 2739 | } 2740 | continue; 2741 | } 2742 | } 2743 | logger.debug("Unhandled '~': {d}", .{param1}); 2744 | continue; 2745 | }, 2746 | else => { 2747 | logger.debug("Unhandled final: {x:02} ({c})", .{ code_point, @as(u8, @intCast(code_point)) }); 2748 | continue; 2749 | }, 2750 | } 2751 | unreachable; 2752 | } 2753 | }, 2754 | .verbatim => { 2755 | self.input_state = .free; 2756 | // Verbatim mode will bypass all mechanisms and just insert the code point. 2757 | self.insertCodePoint(code_point); 2758 | continue; 2759 | }, 2760 | .paste => { 2761 | if (code_point == 27) { 2762 | self.previous_free_state = .paste; 2763 | self.input_state = .got_escape; 2764 | continue; 2765 | } 2766 | 2767 | if (self.on.paste) |_| { 2768 | try self.paste_buffer.container.append(code_point); 2769 | } else { 2770 | self.insertCodePoint(code_point); 2771 | } 2772 | continue; 2773 | }, 2774 | .free => { 2775 | self.previous_free_state = .free; 2776 | if (code_point == 27) { 2777 | try self.callback_machine.keyPressed(self, .{ .code_point = code_point }); 2778 | // Note that this should also deal with explicitly registered keys 2779 | // that would otherwise be interpreted as escapes. 2780 | if (self.callback_machine.shouldProcessLastPressedKey()) { 2781 | self.input_state = .got_escape; 2782 | } 2783 | continue; 2784 | } 2785 | if (code_point == 22) { // ^v 2786 | try self.callback_machine.keyPressed(self, .{ .code_point = code_point }); 2787 | if (self.callback_machine.shouldProcessLastPressedKey()) { 2788 | self.input_state = .verbatim; 2789 | } 2790 | continue; 2791 | } 2792 | }, 2793 | } 2794 | 2795 | // There are no sequences past this point, so short of 'tab', we will want to cleanup the suggestions. 2796 | var should_perform_suggestion_cleanup = true; 2797 | defer if (should_perform_suggestion_cleanup) { 2798 | self.cleanupSuggestions() catch {}; 2799 | }; 2800 | 2801 | // Normally ^D. `stty eof \^n` can change it to ^N (or something else). 2802 | // Process this here since the keybinds might override its behavior. 2803 | // This only applies when the buffer is empty. at any other time, the behavior should be configurable. 2804 | if (code_point == getTermiosCC(self.termios, V.EOF) and self.buffer.container.items.len == 0) { 2805 | _ = self.finishEdit(); 2806 | continue; 2807 | } 2808 | 2809 | try self.callback_machine.keyPressed(self, .{ .code_point = code_point }); 2810 | if (!self.callback_machine.shouldProcessLastPressedKey()) { 2811 | continue; 2812 | } 2813 | 2814 | self.search_offset = 0; // reset search offset on any key 2815 | 2816 | if (code_point == '\t' or reverse_tab) { 2817 | should_perform_suggestion_cleanup = false; 2818 | 2819 | if (self.on.tab_complete == null) { 2820 | continue; 2821 | } 2822 | 2823 | self.times_tab_pressed += 1; 2824 | 2825 | const token_start = self.cursor; 2826 | 2827 | var stderr_buffer: [1024]u8 = undefined; 2828 | var stderr_writer = SystemCapabilities.stderr().writer(&stderr_buffer); 2829 | const stderr = &stderr_writer.interface; 2830 | 2831 | // Ask for completions on the first tab press only. 2832 | if (self.times_tab_pressed == 1) { 2833 | const cb = self.on.tab_complete.?; 2834 | self.suggestion_manager.setSuggestions(try cb.f(cb.context)); 2835 | self.suggestion_manager.last_displayed_suggestion_index = 0; 2836 | self.prompt_lines_at_suggestion_initiation = self.numLines(); 2837 | if (self.suggestion_manager.suggestions.size() == 0) { 2838 | // beep. 2839 | stderr.writeAll("\x07") catch {}; 2840 | } 2841 | } 2842 | 2843 | if (reverse_tab and self.tab_direction != .backward) { 2844 | self.suggestion_manager.previous(); 2845 | self.suggestion_manager.previous(); 2846 | self.tab_direction = .backward; 2847 | } 2848 | if (!reverse_tab and self.tab_direction != .forward) { 2849 | self.suggestion_manager.next(); 2850 | self.suggestion_manager.next(); 2851 | self.tab_direction = .forward; 2852 | } 2853 | reverse_tab = false; 2854 | 2855 | const completion_mode: SuggestionManager.CompletionMode = switch (self.times_tab_pressed) { 2856 | 1 => .complete_prefix, 2857 | 2 => .show_suggestions, 2858 | else => .cycle_suggestions, 2859 | }; 2860 | 2861 | self.insertUtf32(self.remembered_suggestion_static_data.container.items); 2862 | self.remembered_suggestion_static_data.container.clearRetainingCapacity(); 2863 | 2864 | const completion_result = self.suggestion_manager.attemptCompletion(completion_mode, token_start); 2865 | var new_cursor = self.cursor; 2866 | new_cursor = @intCast(@as(isize, @intCast(new_cursor)) + completion_result.new_cursor_offset); 2867 | const start_of_region_to_remove = self.cursor + completion_result.offset_region_to_remove.start - completion_result.offset_region_to_remove.end; 2868 | if (completion_result.offset_region_to_remove.end >= completion_result.offset_region_to_remove.start) { 2869 | for (completion_result.offset_region_to_remove.start..completion_result.offset_region_to_remove.end) |_| { 2870 | self.removeAtIndex(start_of_region_to_remove); 2871 | } 2872 | } 2873 | 2874 | new_cursor -= completion_result.static_offset_from_cursor; 2875 | for (0..completion_result.static_offset_from_cursor) |_| { 2876 | try self.remembered_suggestion_static_data.container.append(self.buffer.container.items[new_cursor]); 2877 | self.removeAtIndex(new_cursor); 2878 | } 2879 | 2880 | self.cursor = new_cursor; 2881 | self.inline_search_cursor = self.cursor; 2882 | self.refresh_needed = true; 2883 | self.chars_touched_in_the_middle += 1; 2884 | 2885 | for (completion_result.insert_storage[0..completion_result.insert_count]) |item| { 2886 | self.insertString(item); 2887 | } 2888 | 2889 | try self.repositionCursor(stderr, false); 2890 | 2891 | if (completion_result.style_to_apply) |style| { 2892 | try self.stylize(Span{ 2893 | .begin = self.suggestion_manager.last_shown_suggestion.start_index, 2894 | .end = self.cursor, 2895 | .mode = .code_point_oriented, 2896 | }, style); 2897 | } 2898 | 2899 | switch (completion_result.new_mode) { 2900 | .@"don't_complete" => { 2901 | self.times_tab_pressed = 0; 2902 | self.remembered_suggestion_static_data.container.clearRetainingCapacity(); 2903 | }, 2904 | .complete_prefix => {}, 2905 | else => { 2906 | self.times_tab_pressed += 1; 2907 | }, 2908 | } 2909 | 2910 | if (self.times_tab_pressed > 1 and self.suggestion_manager.suggestions.size() > 0) { 2911 | if (try self.suggestion_display.cleanup()) { 2912 | try self.repositionCursor(stderr, false); 2913 | } 2914 | self.suggestion_display.prompt_lines_at_suggestion_initiation = self.prompt_lines_at_suggestion_initiation; 2915 | try self.suggestion_display.display(&self.suggestion_manager); 2916 | self.origin_row = self.suggestion_display.origin_row; 2917 | } 2918 | 2919 | if (self.times_tab_pressed > 1) { 2920 | if (self.tab_direction == .forward) { 2921 | self.suggestion_manager.next(); 2922 | } else { 2923 | self.suggestion_manager.previous(); 2924 | } 2925 | } 2926 | 2927 | if (self.suggestion_manager.suggestions.size() < 2 and !completion_result.avoid_committing_to_single_suggestion) { 2928 | try self.repositionCursor(stderr, true); 2929 | try self.cleanupSuggestions(); 2930 | self.remembered_suggestion_static_data.container.clearRetainingCapacity(); 2931 | } 2932 | 2933 | continue; 2934 | } 2935 | 2936 | // If we got here, manually cleanup the suggestions and then insert the new code point. 2937 | self.remembered_suggestion_static_data.container.clearRetainingCapacity(); 2938 | should_perform_suggestion_cleanup = false; 2939 | try self.cleanupSuggestions(); 2940 | self.insertCodePoint(code_point); 2941 | } 2942 | 2943 | if (input_it.i == self.incomplete_data.container.items.len) { 2944 | self.incomplete_data.container.clearAndFree(); 2945 | } else { 2946 | for (input_it.i..self.incomplete_data.container.items.len) |_| { 2947 | _ = self.incomplete_data.container.orderedRemove(input_it.i); 2948 | } 2949 | } 2950 | 2951 | if (self.incomplete_data.container.items.len != 0 and !self.finished) { 2952 | try self.event_loop.loopAction(.retry); 2953 | } 2954 | } 2955 | 2956 | fn handleResizeEvent(self: *Self, reset_origin: bool) InputError!void { 2957 | self.has_origin_reset_scheduled = false; 2958 | if (reset_origin and !self.setOrigin(false)) { 2959 | self.has_origin_reset_scheduled = true; 2960 | try self.event_loop.deferredInvoke(.{ .handle_resize_event = true }); 2961 | return; 2962 | } 2963 | 2964 | self.setOriginValues(self.origin_row, 1); 2965 | 2966 | { 2967 | var stderr_buffer: [1024]u8 = undefined; 2968 | var stderr_writer = SystemCapabilities.stderr().writer(&stderr_buffer); 2969 | const stderr = &stderr_writer.interface; 2970 | 2971 | try self.repositionCursor(stderr, true); 2972 | // FIXME: suggestion_display.redisplay(); 2973 | try self.repositionCursor(stderr, false); 2974 | 2975 | try stderr.flush(); 2976 | } 2977 | 2978 | if (self.is_searching) { 2979 | try self.search_editor.?.resized(); 2980 | } 2981 | } 2982 | 2983 | fn ensureFreeLinesFromOrigin(self: *Self, count: usize) void { 2984 | _ = self; 2985 | _ = count; 2986 | } 2987 | 2988 | fn vtDSR(self: *Self) ![2]usize { 2989 | var buf: [32]u8 = undefined; 2990 | var more_junk_to_read = false; 2991 | var stdin = SystemCapabilities.stdin(); 2992 | var pollfds: [1]SystemCapabilities.pollfd = undefined; 2993 | { 2994 | var pollfd: SystemCapabilities.pollfd = undefined; 2995 | SystemCapabilities.setPollFd(&pollfd, stdin.handle); 2996 | pollfd.events = SystemCapabilities.POLL_IN; 2997 | pollfd.revents = 0; 2998 | pollfds[0] = pollfd; 2999 | } 3000 | 3001 | while (true) { 3002 | more_junk_to_read = false; 3003 | const rc = SystemCapabilities.poll(&pollfds, 1, 0); 3004 | if (rc == 1 and pollfds[0].revents & SystemCapabilities.POLL_IN != 0) { 3005 | const nread = stdin.read(&buf) catch |err| { 3006 | self.finished = true; 3007 | self.input_error = toWrapped(InputError, err); 3008 | return error.ReadFailure; 3009 | }; 3010 | if (nread == 0) { 3011 | break; 3012 | } 3013 | try self.incomplete_data.container.appendSlice(buf[0..nread]); 3014 | more_junk_to_read = true; 3015 | } 3016 | if (!more_junk_to_read) { 3017 | break; 3018 | } 3019 | } 3020 | 3021 | if (self.input_error) |err| { 3022 | return fromWrapped(err); 3023 | } 3024 | 3025 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 3026 | const stderr = &stderr_writer.interface; 3027 | try stderr.writeAll("\x1b[6n"); 3028 | 3029 | var state: enum { 3030 | free, 3031 | saw_esc, 3032 | saw_bracket, 3033 | in_first_coordinate, 3034 | saw_semicolon, 3035 | in_second_coordinate, 3036 | saw_r, 3037 | } = .free; 3038 | var has_error = false; 3039 | var coordinate_buffer: [8]u8 = @splat(0); 3040 | var coordinate_length: usize = 0; 3041 | var row: usize = 0; 3042 | var column: usize = 0; 3043 | 3044 | while (state != .saw_r) { 3045 | var b: [1]u8 = .{0}; 3046 | const length = try stdin.read(&b); 3047 | if (length == 0) { 3048 | logger.debug("Got EOF while reading DSR response", .{}); 3049 | return error.Empty; 3050 | } 3051 | 3052 | const c = b[0]; 3053 | 3054 | switch (state) { 3055 | .free => { 3056 | if (c == 0x1b) { 3057 | state = .saw_esc; 3058 | continue; 3059 | } 3060 | try self.incomplete_data.container.append(c); 3061 | }, 3062 | .saw_esc => { 3063 | if (c == '[') { 3064 | state = .saw_bracket; 3065 | continue; 3066 | } 3067 | try self.incomplete_data.container.append(c); 3068 | state = .free; 3069 | }, 3070 | .saw_bracket => { 3071 | if (c >= '0' and c <= '9') { 3072 | state = .in_first_coordinate; 3073 | coordinate_buffer[0] = c; 3074 | coordinate_length = 1; 3075 | continue; 3076 | } 3077 | try self.incomplete_data.container.append(c); 3078 | state = .free; 3079 | }, 3080 | .in_first_coordinate => { 3081 | if (c >= '0' and c <= '9') { 3082 | if (coordinate_length < coordinate_buffer.len) { 3083 | coordinate_buffer[coordinate_length] = c; 3084 | coordinate_length += 1; 3085 | } 3086 | continue; 3087 | } 3088 | if (c == ';') { 3089 | state = .saw_semicolon; 3090 | // parse the first coordinate 3091 | row = std.fmt.parseInt(u8, coordinate_buffer[0..coordinate_length], 10) catch v: { 3092 | has_error = true; 3093 | break :v 1; 3094 | }; 3095 | coordinate_length = 0; 3096 | continue; 3097 | } 3098 | try self.incomplete_data.container.append(c); 3099 | state = .free; 3100 | }, 3101 | .saw_semicolon => { 3102 | if (c >= '0' and c <= '9') { 3103 | state = .in_second_coordinate; 3104 | coordinate_buffer[0] = c; 3105 | coordinate_length = 1; 3106 | continue; 3107 | } 3108 | try self.incomplete_data.container.append(c); 3109 | state = .free; 3110 | }, 3111 | .in_second_coordinate => { 3112 | if (c >= '0' and c <= '9') { 3113 | if (coordinate_length < coordinate_buffer.len) { 3114 | coordinate_buffer[coordinate_length] = c; 3115 | coordinate_length += 1; 3116 | } 3117 | continue; 3118 | } 3119 | if (c == 'R') { 3120 | // parse the second coordinate 3121 | state = .saw_r; 3122 | column = std.fmt.parseInt(u8, coordinate_buffer[0..coordinate_length], 10) catch v: { 3123 | has_error = true; 3124 | break :v 1; 3125 | }; 3126 | continue; 3127 | } 3128 | try self.incomplete_data.container.append(c); 3129 | }, 3130 | .saw_r => unreachable, 3131 | } 3132 | } 3133 | 3134 | if (has_error) { 3135 | logger.debug("Couldn't parse DSR response", .{}); 3136 | } 3137 | 3138 | return .{ row, column }; 3139 | } 3140 | 3141 | fn removeAtIndex(self: *Self, index: usize) void { 3142 | const c = self.buffer.container.orderedRemove(index); 3143 | if (c == '\n') { 3144 | self.extra_forward_lines += 1; 3145 | } 3146 | self.chars_touched_in_the_middle += 1; 3147 | } 3148 | 3149 | fn reset(self: *Self) !void { 3150 | try self.cached_buffer_metrics.reset(); 3151 | self.cached_prompt_valid = false; 3152 | self.cursor = 0; 3153 | self.drawn_cursor = 0; 3154 | self.inline_search_cursor = 0; 3155 | self.search_offset = 0; 3156 | self.search_offset_state = .unbiased; 3157 | self.old_prompt_metrics = self.cached_prompt_metrics; 3158 | self.origin_row = 0; 3159 | self.origin_column = 0; 3160 | self.prompt_lines_at_suggestion_initiation = 0; 3161 | self.refresh_needed = true; 3162 | self.input_error = null; 3163 | self.returned_line = &.{}; 3164 | self.chars_touched_in_the_middle = 0; 3165 | self.drawn_end_of_line_offset = 0; 3166 | self.current_spans.deinit(); 3167 | self.current_spans = .init(self.allocator); 3168 | if (self.drawn_spans) |*spans| { 3169 | spans.deinit(); 3170 | } 3171 | self.drawn_spans = null; 3172 | self.paste_buffer.container.clearAndFree(); 3173 | } 3174 | 3175 | fn search(self: *Self, phrase: []const u8, allow_empty: bool, from_beginning: bool) bool { 3176 | var last_matching_offset: i32 = -1; 3177 | var found: bool = false; 3178 | 3179 | if (allow_empty or phrase.len > 0) { 3180 | var search_offset = self.search_offset; 3181 | var i = self.history_cursor; 3182 | while (i > 0) : (i -= 1) { 3183 | const entry = self.history.container.items[i - 1]; 3184 | const contains = if (from_beginning) 3185 | std.mem.startsWith(u8, entry.entry, phrase) 3186 | else 3187 | std.mem.containsAtLeast(u8, entry.entry, 1, phrase); 3188 | if (contains) { 3189 | last_matching_offset = @as(i32, @intCast(i)) - 1; 3190 | if (search_offset == 0) { 3191 | found = true; 3192 | break; 3193 | } 3194 | if (search_offset > 0) { 3195 | search_offset -= 1; 3196 | } 3197 | } 3198 | } 3199 | 3200 | if (!found) { 3201 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 3202 | const stderr = &stderr_writer.interface; 3203 | stderr.writeAll("\x07") catch {}; 3204 | } 3205 | } 3206 | 3207 | if (found) { 3208 | // We're gonna clear the buffer, so mark the entire thing touched. 3209 | self.chars_touched_in_the_middle = self.buffer.container.items.len; 3210 | self.buffer.container.clearRetainingCapacity(); 3211 | self.cursor = 0; 3212 | self.insertString(self.history.container.items[@intCast(last_matching_offset)].entry); 3213 | // Always needed. 3214 | self.refresh_needed = true; 3215 | } 3216 | 3217 | return found; 3218 | } 3219 | 3220 | fn endSearch(self: *Self) void { 3221 | self.is_searching = false; 3222 | self.refresh_needed = true; 3223 | self.search_offset = 0; 3224 | if (self.reset_buffer_on_search_end) { 3225 | self.buffer.container.clearRetainingCapacity(); 3226 | self.insertUtf32(self.pre_search_buffer.container.items); 3227 | self.cursor = self.pre_search_cursor; 3228 | } 3229 | self.reset_buffer_on_search_end = true; 3230 | if (self.search_editor) |e| { 3231 | e.deinit(); 3232 | } 3233 | self.search_editor = null; 3234 | } 3235 | 3236 | fn findApplicableStyle(self: *Self, index: usize) Style { 3237 | var style: Style = .reset; 3238 | var it = self.current_spans.starting.container.iterator(); 3239 | while (it.next()) |entry| { 3240 | unifyStylesInto(entry, index, &style); 3241 | } 3242 | 3243 | return style; 3244 | } 3245 | 3246 | fn unifyStylesInto(styles: std.AutoHashMap(usize, AutoHashMap(usize, Style)).Entry, offset: usize, target: *Style) void { 3247 | if (styles.key_ptr.* >= offset) { 3248 | return; 3249 | } 3250 | 3251 | var it = styles.value_ptr.container.unmanaged.iterator(); 3252 | while (it.next()) |entry| { 3253 | if (entry.key_ptr.* <= offset) { 3254 | return; 3255 | } 3256 | target.unifyWith(entry.value_ptr.*, true); 3257 | } 3258 | } 3259 | 3260 | fn refreshDisplay(self: *Self) !void { 3261 | if (self.was_interrupted) { 3262 | self.was_interrupted = false; 3263 | return; 3264 | } 3265 | 3266 | var stderr_buffer: [1024]u8 = undefined; 3267 | var stderr_writer = SystemCapabilities.stderr().writer(&stderr_buffer); 3268 | const stderr = &stderr_writer.interface; 3269 | defer { 3270 | self.shown_lines = self.currentPromptMetrics().linesWithAddition(self.cached_buffer_metrics, self.num_columns); 3271 | stderr.flush() catch {}; 3272 | } 3273 | 3274 | const has_cleaned_up = false; 3275 | // Someone changed the window size, figure it out 3276 | // and react to it. We might need to redraw. 3277 | if (self.was_resized) { 3278 | if (self.previous_num_columns != self.num_columns) { 3279 | // We need to cleanup and redo everything. 3280 | self.cached_prompt_valid = false; 3281 | self.refresh_needed = true; 3282 | std.mem.swap(usize, &self.previous_num_columns, &self.num_columns); 3283 | self.recalculateOrigin(); 3284 | try self.cleanup(); 3285 | std.mem.swap(usize, &self.previous_num_columns, &self.num_columns); 3286 | self.refresh_needed = true; 3287 | } 3288 | self.was_resized = false; 3289 | } 3290 | 3291 | // We might be at the last line, and more than one line; 3292 | // Refreshing the display will cause the terminal to scroll, 3293 | // so note that fact and bring the origin up, making sure to 3294 | // reserve the space for however many lines we move it up. 3295 | const current_num_lines = self.numLines(); 3296 | if (self.origin_row + current_num_lines > self.num_lines) { 3297 | if (current_num_lines > self.num_lines) { 3298 | for (0..self.num_lines) |_| { 3299 | try stderr.writeAll("\n"); 3300 | } 3301 | self.origin_row = 0; 3302 | } else { 3303 | const old_origin_row = self.origin_row; 3304 | self.origin_row = self.num_lines - current_num_lines + 1; 3305 | for (0..old_origin_row - self.origin_row) |_| { 3306 | try stderr.writeAll("\n"); 3307 | } 3308 | } 3309 | } 3310 | 3311 | // Do not call hook on pure cursor movements. 3312 | if (self.cached_prompt_valid and !self.refresh_needed and self.pending_chars.container.items.len == 0) { 3313 | try self.repositionCursor(stderr, false); 3314 | self.cached_buffer_metrics.deinit(); 3315 | self.cached_buffer_metrics = try self.actualRenderedUnicodeStringMetrics(self.buffer.container.items); 3316 | self.drawn_end_of_line_offset = self.buffer.size(); 3317 | return; 3318 | } 3319 | 3320 | if (self.on.display_refresh) |*cb| { 3321 | cb.f(cb.context); 3322 | } 3323 | 3324 | var empty_styles: AutoHashMap(usize, Style) = .init(self.allocator); 3325 | defer empty_styles.deinit(); 3326 | 3327 | if (self.cached_prompt_valid) { 3328 | if (!self.refresh_needed and self.cursor == self.buffer.size()) { 3329 | // Just write the characters out and continue, 3330 | // no need to refresh anything else. 3331 | for (self.drawn_cursor..self.buffer.size()) |i| { 3332 | try self.applyStyles(&empty_styles, stderr, i); 3333 | try self.printCharacterAt(i, stderr); 3334 | } 3335 | try vtApplyStyle(.reset, stderr, true); 3336 | self.pending_chars.container.clearAndFree(); 3337 | self.drawn_cursor = self.cursor; 3338 | self.drawn_end_of_line_offset = self.buffer.size(); 3339 | self.cached_buffer_metrics.deinit(); 3340 | self.cached_buffer_metrics = try self.actualRenderedUnicodeStringMetrics(self.buffer.container.items); 3341 | return; 3342 | } 3343 | } 3344 | 3345 | // Ouch, reflow entire line. 3346 | if (!has_cleaned_up) { 3347 | try self.cleanup(); 3348 | } 3349 | 3350 | try self.vtMoveAbsolute(self.origin_row, self.origin_column, stderr); 3351 | 3352 | try stderr.writeAll(self.new_prompt.container.items); 3353 | 3354 | try self.vtClearToEndOfLine(stderr); 3355 | 3356 | for (0..self.buffer.size()) |i| { 3357 | try self.applyStyles(&empty_styles, stderr, i); 3358 | try self.printCharacterAt(i, stderr); 3359 | } 3360 | 3361 | try vtApplyStyle(.reset, stderr, true); 3362 | 3363 | self.pending_chars.container.clearAndFree(); 3364 | self.refresh_needed = false; 3365 | self.cached_buffer_metrics.deinit(); 3366 | self.cached_buffer_metrics = try self.actualRenderedUnicodeStringMetrics(self.buffer.container.items); 3367 | self.chars_touched_in_the_middle = 0; 3368 | self.drawn_end_of_line_offset = self.buffer.size(); 3369 | self.cached_prompt_valid = true; 3370 | 3371 | try self.repositionCursor(stderr, false); 3372 | } 3373 | 3374 | pub fn setHandler(self: *Self, handler: anytype) void { 3375 | const T = @TypeOf(handler); 3376 | if (@typeInfo(T) != .pointer) { 3377 | @compileError("Handler must be a pointer type"); 3378 | } 3379 | 3380 | const InnerT = @TypeOf(handler.*); 3381 | 3382 | inline for (@typeInfo(InnerT).@"struct".decls) |decl| { 3383 | const h = &@field(self.on, decl.name); 3384 | h.* = .{ 3385 | .f = &@TypeOf(h.*.?).makeHandler(T, InnerT, decl.name).theHandler, 3386 | .context = handler, 3387 | }; 3388 | } 3389 | } 3390 | 3391 | fn applyStyles(self: *Self, empty_styles: *AutoHashMap(usize, Style), writer: *std.Io.Writer, index: usize) !void { 3392 | const HM = AutoHashMap(usize, AutoHashMap(usize, Style)); 3393 | var ends = (self.current_spans.ending.container.getEntry(index) orelse HM.Entry{ 3394 | .value_ptr = empty_styles, 3395 | .key_ptr = undefined, 3396 | }).value_ptr; 3397 | 3398 | var starts = (self.current_spans.starting.container.getEntry(index) orelse HM.Entry{ 3399 | .value_ptr = empty_styles, 3400 | .key_ptr = undefined, 3401 | }).value_ptr; 3402 | 3403 | if (ends.container.count() > 0) { 3404 | var style: Style = .{}; 3405 | 3406 | var it = ends.container.unmanaged.iterator(); 3407 | while (it.next()) |entry| { 3408 | style.unifyWith(entry.value_ptr.*, false); 3409 | } 3410 | 3411 | // Disable any style that should be turned off. 3412 | try vtApplyStyle(style, writer, false); 3413 | 3414 | // Reapply styles for overlapping spans that include this one. 3415 | style = self.findApplicableStyle(index); 3416 | try vtApplyStyle(style, writer, true); 3417 | } 3418 | 3419 | if (starts.container.count() > 0) { 3420 | var style: Style = .{}; 3421 | var it = starts.container.unmanaged.iterator(); 3422 | while (it.next()) |entry| { 3423 | style.unifyWith(entry.value_ptr.*, true); 3424 | } 3425 | 3426 | // Set new styles. 3427 | try vtApplyStyle(style, writer, true); 3428 | } 3429 | } 3430 | 3431 | fn printCharacterAt(self: *Self, index: usize, writer: *std.Io.Writer) !void { 3432 | return self.printSingleCharacter(self.buffer.container.items[index], writer); 3433 | } 3434 | 3435 | fn printSingleCharacter(self: *Self, code_point: u32, writer: *std.Io.Writer) !void { 3436 | var buffer: std.array_list.Managed(u8) = .init(self.allocator); 3437 | defer buffer.deinit(); 3438 | 3439 | const should_print_masked = isAsciiControl(code_point) and code_point != '\n'; 3440 | const should_print_caret = code_point < 64 and should_print_masked; 3441 | if (should_print_caret) { 3442 | try buffer.append('^'); 3443 | try buffer.append(@intCast(code_point + 64)); 3444 | } else { 3445 | const c: u21 = @intCast(code_point); 3446 | const length = try std.unicode.utf8CodepointSequenceLength(c); 3447 | try buffer.appendNTimes(0, length); 3448 | _ = try std.unicode.utf8Encode(c, buffer.items[buffer.items.len - length .. buffer.items.len]); 3449 | } 3450 | 3451 | try writer.writeAll(buffer.items); 3452 | } 3453 | 3454 | fn cleanup(self: *Self) !void { 3455 | const current_buffer_metrics = try self.actualRenderedUnicodeStringMetrics(self.buffer.container.items); 3456 | self.cached_buffer_metrics.deinit(); 3457 | self.cached_buffer_metrics = current_buffer_metrics; 3458 | const new_lines = self.currentPromptMetrics().linesWithAddition(current_buffer_metrics, self.num_columns); 3459 | const shown_lines = self.shown_lines; 3460 | if (new_lines < shown_lines) { 3461 | self.extra_forward_lines = @max(shown_lines - new_lines, self.extra_forward_lines); 3462 | } 3463 | 3464 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 3465 | const stderr = &stderr_writer.interface; 3466 | try self.repositionCursor(stderr, true); 3467 | const current_line = self.numLines(); 3468 | try self.vtClearLines(current_line, self.extra_forward_lines, stderr); 3469 | self.extra_forward_lines = 0; 3470 | try self.repositionCursor(stderr, false); 3471 | } 3472 | 3473 | fn cleanupSuggestions(self: *Self) !void { 3474 | if (self.times_tab_pressed != 0) { 3475 | self.stylize(Span{ 3476 | .begin = self.suggestion_manager.last_shown_suggestion.start_index, 3477 | .end = self.cursor, 3478 | .mode = .code_point_oriented, 3479 | }, self.suggestion_manager.last_shown_suggestion.style) catch {}; 3480 | 3481 | if (try self.suggestion_display.cleanup()) { 3482 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 3483 | const stderr = &stderr_writer.interface; 3484 | try self.repositionCursor(stderr, false); 3485 | self.refresh_needed = true; 3486 | } 3487 | self.suggestion_manager.reset(); 3488 | self.suggestion_display.finish(); 3489 | } 3490 | self.times_tab_pressed = 0; 3491 | } 3492 | 3493 | fn reallyQuitEventLoop(self: *Self) !void { 3494 | self.finished = false; 3495 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 3496 | const stderr = &stderr_writer.interface; 3497 | try self.repositionCursor(stderr, true); 3498 | try stderr.writeAll("\n"); 3499 | 3500 | const str = try self.getBufferedLine(); 3501 | self.buffer.container.clearAndFree(); 3502 | self.chars_touched_in_the_middle = 0; 3503 | self.is_editing = false; 3504 | self.returned_line = str; 3505 | 3506 | if (self.initialized) { 3507 | self.restore() catch {}; 3508 | } 3509 | 3510 | try self.event_loop.loopAction(.exit); 3511 | } 3512 | 3513 | fn restore(self: *Self) !void { 3514 | if (!self.initialized) unreachable; 3515 | 3516 | try setTermios(self.default_termios); 3517 | self.initialized = false; 3518 | if (self.configuration.enable_bracketed_paste) { 3519 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 3520 | const stderr = &stderr_writer.interface; 3521 | try stderr.writeAll("\x1b[?2004l"); 3522 | } 3523 | } 3524 | 3525 | fn currentPromptMetrics(self: *Self) StringMetrics { 3526 | if (!self.cached_prompt_valid) { 3527 | return self.old_prompt_metrics; 3528 | } 3529 | 3530 | return self.cached_prompt_metrics; 3531 | } 3532 | 3533 | fn numLines(self: *Self) usize { 3534 | return self.currentPromptMetrics().linesWithAddition(self.cached_buffer_metrics, self.num_columns); 3535 | } 3536 | 3537 | fn cursorLine(self: *Self) !usize { 3538 | var cursor = self.drawn_cursor; 3539 | if (cursor > self.cursor) { 3540 | cursor = self.cursor; 3541 | } 3542 | var metrics = try self.actualRenderedUnicodeStringMetrics(self.buffer.container.items[0..cursor]); 3543 | defer metrics.deinit(); 3544 | return self.currentPromptMetrics().linesWithAddition(metrics, self.num_columns); 3545 | } 3546 | 3547 | fn offsetInLine(self: *Self) !usize { 3548 | var cursor = self.drawn_cursor; 3549 | if (cursor > self.cursor) { 3550 | cursor = self.cursor; 3551 | } 3552 | var metrics = try self.actualRenderedUnicodeStringMetrics(self.buffer.container.items[0..cursor]); 3553 | defer metrics.deinit(); 3554 | return self.currentPromptMetrics().offsetWithAddition(metrics, self.num_columns); 3555 | } 3556 | 3557 | fn setOrigin(self: *Self, quit_on_error: bool) bool { 3558 | const position = self.vtDSR() catch |err| { 3559 | if (quit_on_error) { 3560 | self.input_error = toWrapped(InputError, err); 3561 | _ = self.finish(); 3562 | } 3563 | return false; 3564 | }; 3565 | self.setOriginValues(position[0], position[1]); 3566 | return true; 3567 | } 3568 | 3569 | fn setOriginValues(self: *Self, row: usize, column: usize) void { 3570 | self.origin_row = row; 3571 | self.origin_column = column; 3572 | self.suggestion_display.origin_row = row; 3573 | self.suggestion_display.origin_column = column; 3574 | } 3575 | 3576 | fn recalculateOrigin(self: *Self) void { 3577 | _ = self; 3578 | } 3579 | 3580 | fn repositionCursor(self: *Self, writer: *std.Io.Writer, to_end: bool) !void { 3581 | var cursor = self.cursor; 3582 | const saved_cursor = cursor; 3583 | if (to_end) { 3584 | cursor = self.buffer.size(); 3585 | } 3586 | 3587 | self.cursor = cursor; 3588 | self.drawn_cursor = cursor; 3589 | 3590 | const line = try self.cursorLine() - 1; 3591 | const column = try self.offsetInLine(); 3592 | 3593 | self.ensureFreeLinesFromOrigin(line); 3594 | 3595 | try self.vtMoveAbsolute(line + self.origin_row, column + self.origin_column, writer); 3596 | 3597 | self.cursor = saved_cursor; 3598 | } 3599 | 3600 | const CodePointRange = struct { 3601 | start: u32, 3602 | end: u32, 3603 | }; 3604 | 3605 | fn byteOffsetRangeToCodePointOffsetRange( 3606 | self: *Self, 3607 | byte_start: usize, 3608 | byte_end: usize, 3609 | code_point_scan_offset: usize, 3610 | reverse: bool, 3611 | ) CodePointRange { 3612 | _ = self; 3613 | _ = byte_start; 3614 | _ = byte_end; 3615 | _ = code_point_scan_offset; 3616 | _ = reverse; 3617 | return undefined; 3618 | } 3619 | 3620 | fn getTerminalSize(self: *Self) void { 3621 | self.num_columns = 80; 3622 | self.num_lines = 24; 3623 | defer { 3624 | self.suggestion_display.columns = self.num_columns; 3625 | self.suggestion_display.lines = self.num_lines; 3626 | } 3627 | if (!is_windows) { 3628 | const ws = SystemCapabilities.getWinsize(SystemCapabilities.stdin().handle) catch { 3629 | return; 3630 | }; 3631 | self.num_columns = ws.col; 3632 | self.num_lines = ws.row; 3633 | } 3634 | } 3635 | 3636 | pub fn goEnd(self: *Self) bool { 3637 | self.cursor = self.buffer.size(); 3638 | self.inline_search_cursor = self.cursor; 3639 | self.search_offset = 0; 3640 | return false; 3641 | } 3642 | 3643 | pub fn goHome(self: *Self) bool { 3644 | self.cursor = 0; 3645 | self.inline_search_cursor = self.cursor; 3646 | self.search_offset = 0; 3647 | return false; 3648 | } 3649 | 3650 | pub fn clearScreen(self: *Self) bool { 3651 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 3652 | const stderr = &stderr_writer.interface; 3653 | stderr.writeAll("\x1b[3J\x1b[H\x1b[2J") catch {}; 3654 | 3655 | self.vtMoveAbsolute(1, 1, stderr) catch {}; 3656 | self.setOriginValues(1, 1); 3657 | self.refresh_needed = true; 3658 | self.cached_prompt_valid = false; 3659 | return false; 3660 | } 3661 | 3662 | pub fn eraseCharacterBackwards(self: *Self) bool { 3663 | if (self.is_searching) { 3664 | return false; 3665 | } 3666 | 3667 | if (self.cursor == 0) { 3668 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 3669 | const stderr = &stderr_writer.interface; 3670 | stderr.writeAll("\x07") catch {}; // \a BEL 3671 | return false; 3672 | } 3673 | 3674 | self.removeAtIndex(self.cursor - 1); 3675 | self.cursor -= 1; 3676 | self.inline_search_cursor = self.cursor; 3677 | self.refresh_needed = true; 3678 | return false; 3679 | } 3680 | 3681 | pub fn eraseCharacterForwards(self: *Self) bool { 3682 | if (self.is_searching) { 3683 | return false; 3684 | } 3685 | 3686 | if (self.cursor == self.buffer.size()) { 3687 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 3688 | const stderr = &stderr_writer.interface; 3689 | stderr.writeAll("\x07") catch {}; // \a BEL 3690 | return false; 3691 | } 3692 | 3693 | self.removeAtIndex(self.cursor); 3694 | self.refresh_needed = true; 3695 | return false; 3696 | } 3697 | 3698 | pub fn cursorLeftCharacter(self: *Self) bool { 3699 | if (self.cursor == 0) { 3700 | return false; 3701 | } 3702 | 3703 | self.cursor -= 1; 3704 | self.inline_search_cursor = self.cursor; 3705 | return false; 3706 | } 3707 | 3708 | pub fn cursorRightCharacter(self: *Self) bool { 3709 | if (self.cursor == self.buffer.size()) { 3710 | return false; 3711 | } 3712 | 3713 | self.cursor += 1; 3714 | self.inline_search_cursor = self.cursor; 3715 | return false; 3716 | } 3717 | 3718 | pub fn searchForwards(self: *Self) bool { 3719 | const p = sane.checkpoint(&self.inline_search_cursor); 3720 | defer p.restore(); 3721 | 3722 | var builder: StringBuilder = .init(self.allocator); 3723 | defer builder.deinit(); 3724 | 3725 | builder.appendUtf32Slice(self.buffer.container.items[0..self.inline_search_cursor]) catch return false; 3726 | const search_phrase = builder.toSlice(); 3727 | 3728 | if (self.search_offset_state == .backwards) { 3729 | if (self.search_offset > 0) { 3730 | self.search_offset -= 1; 3731 | } 3732 | } 3733 | 3734 | if (self.search_offset > 0) { 3735 | var p1 = sane.checkpoint(&self.search_offset); 3736 | defer p1.restore(); 3737 | 3738 | self.search_offset -= 1; 3739 | if (self.search(search_phrase, true, true)) { 3740 | self.search_offset_state = .forwards; 3741 | p1.v = self.search_offset; 3742 | } else { 3743 | self.search_offset_state = .unbiased; 3744 | } 3745 | } else { 3746 | self.search_offset_state = .unbiased; 3747 | self.chars_touched_in_the_middle = self.buffer.size(); 3748 | self.cursor = 0; 3749 | self.buffer.container.clearRetainingCapacity(); 3750 | self.insertString(search_phrase); 3751 | self.refresh_needed = true; 3752 | } 3753 | 3754 | return false; 3755 | } 3756 | 3757 | pub fn searchBackwards(self: *Self) bool { 3758 | const p = sane.checkpoint(&self.inline_search_cursor); 3759 | defer p.restore(); 3760 | 3761 | var builder: StringBuilder = .init(self.allocator); 3762 | defer builder.deinit(); 3763 | 3764 | builder.appendUtf32Slice(self.buffer.container.items[0..self.inline_search_cursor]) catch return false; 3765 | const search_phrase = builder.toSlice(); 3766 | 3767 | if (self.search_offset_state == .forwards) { 3768 | self.search_offset += 1; 3769 | } 3770 | 3771 | if (self.search(search_phrase, true, true)) { 3772 | self.search_offset_state = .backwards; 3773 | self.search_offset += 1; 3774 | } else { 3775 | self.search_offset_state = .unbiased; 3776 | if (self.search_offset > 0) { 3777 | self.search_offset -= 1; 3778 | } 3779 | } 3780 | 3781 | return false; 3782 | } 3783 | 3784 | pub fn finishEdit(self: *Self) bool { 3785 | self.prohibitInput(); 3786 | defer self.allowInput(); 3787 | 3788 | var stderr_writer = SystemCapabilities.stderr().writer(&.{}); 3789 | const stderr = &stderr_writer.interface; 3790 | stderr.writeAll("\n") catch {}; 3791 | if (!self.always_refresh) { 3792 | self.input_error = toWrapped(InputError, error.Eof); 3793 | _ = self.finish(); 3794 | self.reallyQuitEventLoop() catch {}; 3795 | } 3796 | return false; 3797 | } 3798 | 3799 | pub fn eraseToEnd(self: *Self) bool { 3800 | if (self.cursor == self.buffer.size()) { 3801 | return false; 3802 | } 3803 | 3804 | while (self.cursor < self.buffer.size()) { 3805 | _ = self.eraseCharacterForwards(); 3806 | } 3807 | 3808 | return false; 3809 | } 3810 | 3811 | pub fn transposeCharacters(self: *Self) bool { 3812 | if (self.cursor > 0 and self.buffer.size() >= 2) { 3813 | if (self.cursor < self.buffer.size()) { 3814 | self.cursor += 1; 3815 | } 3816 | 3817 | std.mem.swap(u32, &self.buffer.container.items[self.cursor - 1], &self.buffer.container.items[self.cursor - 2]); 3818 | self.refresh_needed = true; 3819 | self.chars_touched_in_the_middle += 2; 3820 | } 3821 | return false; 3822 | } 3823 | 3824 | pub fn cursorLeftWord(self: *Self) bool { 3825 | var has_seen_alnum = false; 3826 | while (self.cursor > 0) { 3827 | if (!isAsciiAlnum(self.buffer.container.items[self.cursor - 1])) { 3828 | if (has_seen_alnum) { 3829 | break; 3830 | } 3831 | } else { 3832 | has_seen_alnum = true; 3833 | } 3834 | 3835 | self.cursor -= 1; 3836 | } 3837 | self.inline_search_cursor = self.cursor; 3838 | return false; 3839 | } 3840 | 3841 | pub fn cursorLeftNonspaceWord(self: *Self) bool { 3842 | var has_seen_space = false; 3843 | while (self.cursor > 0) { 3844 | if (isAsciiSpace(self.buffer.container.items[self.cursor - 1])) { 3845 | if (has_seen_space) { 3846 | break; 3847 | } 3848 | } else { 3849 | has_seen_space = true; 3850 | } 3851 | 3852 | self.cursor -= 1; 3853 | } 3854 | self.inline_search_cursor = self.cursor; 3855 | return false; 3856 | } 3857 | 3858 | pub fn cursorRightWord(self: *Self) bool { 3859 | var has_seen_alnum = false; 3860 | while (self.cursor < self.buffer.size()) { 3861 | if (!isAsciiAlnum(self.buffer.container.items[self.cursor])) { 3862 | if (has_seen_alnum) { 3863 | break; 3864 | } 3865 | } else { 3866 | has_seen_alnum = true; 3867 | } 3868 | 3869 | self.cursor += 1; 3870 | } 3871 | self.inline_search_cursor = self.cursor; 3872 | self.search_offset = 0; 3873 | return false; 3874 | } 3875 | 3876 | pub fn cursorRightNonspaceWord(self: *Self) bool { 3877 | var has_seen_space = false; 3878 | while (self.cursor < self.buffer.size()) { 3879 | if (isAsciiSpace(self.buffer.container.items[self.cursor])) { 3880 | if (has_seen_space) { 3881 | break; 3882 | } 3883 | } else { 3884 | has_seen_space = true; 3885 | } 3886 | 3887 | self.cursor += 1; 3888 | } 3889 | self.inline_search_cursor = self.cursor; 3890 | self.search_offset = 0; 3891 | return false; 3892 | } 3893 | 3894 | pub fn eraseAlnumWordBackwards(self: *Self) bool { 3895 | if (self.cursor == 0) { 3896 | return false; 3897 | } 3898 | 3899 | var has_seen_alnum = false; 3900 | while (self.cursor > 0) { 3901 | if (!isAsciiAlnum(self.buffer.container.items[self.cursor - 1])) { 3902 | if (has_seen_alnum) { 3903 | break; 3904 | } 3905 | } else { 3906 | has_seen_alnum = true; 3907 | } 3908 | 3909 | _ = self.eraseCharacterBackwards(); 3910 | } 3911 | return false; 3912 | } 3913 | 3914 | pub fn eraseAlnumWordForwards(self: *Self) bool { 3915 | if (self.cursor == self.buffer.size()) { 3916 | return false; 3917 | } 3918 | 3919 | var has_seen_alnum = false; 3920 | while (self.cursor < self.buffer.size()) { 3921 | if (!isAsciiAlnum(self.buffer.container.items[self.cursor])) { 3922 | if (has_seen_alnum) { 3923 | break; 3924 | } 3925 | } else { 3926 | has_seen_alnum = true; 3927 | } 3928 | 3929 | _ = self.eraseCharacterForwards(); 3930 | } 3931 | return false; 3932 | } 3933 | 3934 | pub fn eraseWordBackwards(self: *Self) bool { 3935 | if (self.cursor == 0) { 3936 | return false; 3937 | } 3938 | 3939 | var has_seen_nonspace = false; 3940 | while (self.cursor > 0) { 3941 | if (isAsciiSpace(self.buffer.container.items[self.cursor - 1])) { 3942 | if (has_seen_nonspace) { 3943 | break; 3944 | } 3945 | } else { 3946 | has_seen_nonspace = true; 3947 | } 3948 | 3949 | _ = self.eraseCharacterBackwards(); 3950 | } 3951 | return false; 3952 | } 3953 | 3954 | pub fn capitalizeWord(self: *Self) bool { 3955 | self.caseChangeWord(.capital); 3956 | return false; 3957 | } 3958 | 3959 | pub fn lowercaseWord(self: *Self) bool { 3960 | self.caseChangeWord(.lower); 3961 | return false; 3962 | } 3963 | 3964 | pub fn uppercaseWord(self: *Self) bool { 3965 | self.caseChangeWord(.upper); 3966 | return false; 3967 | } 3968 | 3969 | pub fn killLine(self: *Self) bool { 3970 | if (self.cursor == 0) { 3971 | return false; 3972 | } 3973 | 3974 | for (0..self.cursor) |_| { 3975 | self.removeAtIndex(0); 3976 | } 3977 | self.cursor = 0; 3978 | self.inline_search_cursor = 0; 3979 | self.refresh_needed = true; 3980 | return false; 3981 | } 3982 | 3983 | fn caseChangeWord(self: *Self, op: enum { lower, upper, capital }) void { 3984 | while (self.cursor < self.buffer.size() and !isAsciiAlnum(self.buffer.container.items[self.cursor])) { 3985 | self.cursor += 1; 3986 | } 3987 | const start = self.cursor; 3988 | while (self.cursor < self.buffer.size() and isAsciiAlnum(self.buffer.container.items[self.cursor])) { 3989 | if (op == .upper or (op == .capital and self.cursor == start)) { 3990 | self.buffer.container.items[self.cursor] = std.ascii.toUpper(@intCast(self.buffer.container.items[self.cursor])); 3991 | } else { 3992 | self.buffer.container.items[self.cursor] = std.ascii.toLower(@intCast(self.buffer.container.items[self.cursor])); 3993 | } 3994 | self.cursor += 1; 3995 | } 3996 | 3997 | self.refresh_needed = true; 3998 | self.chars_touched_in_the_middle += 1; 3999 | } 4000 | }; 4001 | --------------------------------------------------------------------------------