From 963d1fa7bd2d74c951859f27e4fb01eb71a77e8d Mon Sep 17 00:00:00 2001 From: Uko Kokņevičs Date: Sun, 2 Jan 2022 05:25:20 +0200 Subject: Improved input --- src/Buffer.zig | 4 + src/Editor.zig | 150 ++---------------------------------- src/KeyReader.zig | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/key.zig | 158 +++++++++++++++++++++++++------------- src/key_state.zig | 23 +++++- src/main.zig | 3 +- 6 files changed, 359 insertions(+), 200 deletions(-) create mode 100644 src/KeyReader.zig (limited to 'src') diff --git a/src/Buffer.zig b/src/Buffer.zig index 7f6e7d7..7afdb05 100644 --- a/src/Buffer.zig +++ b/src/Buffer.zig @@ -141,6 +141,10 @@ pub fn deleteAllRows(self: *Buffer) void { } pub fn deleteRow(self: *Buffer, at: usize) void { + if (at == self.rows.items.len) { + return; + } + self.dirty = true; self.rows.orderedRemove(at).deinit(); diff --git a/src/Editor.zig b/src/Editor.zig index 4b563ad..99ec932 100644 --- a/src/Editor.zig +++ b/src/Editor.zig @@ -6,6 +6,7 @@ const ArrayList = std.ArrayList; const Buffer = es.Buffer; const Editor = @This(); const Key = es.Key; +const KeyReader = es.KeyReader; const KeyState = es.key_state.KeyState; const StringBuilder = es.StringBuilder; const StringHashMap = std.StringHashMap; @@ -23,7 +24,7 @@ statusmsg_time: i64, current_state: KeyState, -key_buffer: ArrayList(Key), +key_reader: KeyReader, should_exit: bool, pub fn init(allocator: Allocator) !Editor { @@ -41,7 +42,7 @@ pub fn init(allocator: Allocator) !Editor { .current_state = es.key_state.defaultState, - .key_buffer = ArrayList(Key).init(allocator), + .key_reader = KeyReader.init(allocator), .should_exit = false, }; errdefer self.deinit(); @@ -66,7 +67,7 @@ pub fn deinit(self: *Editor) void { self.allocator.free(statusmsg); } - self.key_buffer.deinit(); + self.key_reader.deinit(); self.* = undefined; } @@ -195,7 +196,7 @@ pub fn openFile(self: *Editor) !void { } pub fn processKeypress(self: *Editor) !void { - const key = try self.readKey(); + const key = try self.key_reader.readKey(); try self.current_state(self, self.buffer, key); } @@ -220,7 +221,7 @@ pub fn promptEx( // TODO: Navigation // TODO: Draw the cursor - const key = try self.readKey(); + const key = try self.key_reader.readKey(); switch (key) { Key.delete, Key.backspace => _ = buf.popOrNull(), Key.ctrl('g') => { @@ -430,142 +431,3 @@ fn parseUnsignedOptDefault(comptime T: type, buf_opt: ?[]const u8, radix: u8, de return default; } } - -fn readByte(reader: std.fs.File) !?u8 { - var buf = [_]u8{undefined}; - if (1 != try reader.read(&buf)) { - return null; - } else { - return buf[0]; - } -} - -fn readByteBlocking(reader: std.fs.File) !u8 { - // No we do not care about possible EOF on stdin, don't run the editor with redirected stdin. - var char = try readByte(reader); - while (char == null) : (char = try readByte(reader)) { - std.os.sched_yield() catch {}; // :) - } - - return char.?; -} - -fn readKey(self: *Editor) !Key { - if (self.key_buffer.items.len > 0) { - return self.key_buffer.pop(); - } - - const std_in = std.io.getStdIn(); - - const char1 = try readByteBlocking(std_in); - if (char1 != '\x1b') { - return Key.char(char1); - } - - // TODO: This is a bad way of parsing. - const char2 = (try readByte(std_in)) orelse { return Key.escape; }; - if (char2 == '[') { - const char3 = (try readByte(std_in)) orelse { return Key.meta('['); }; - if (char3 >= '0' and char3 <= '9') { - const char4 = (try readByte(std_in)) orelse { - std.log.err("Unknown terminal sequence '^[[{c}'", .{char3}); - try self.key_buffer.append(Key.char(char3)); - return Key.meta('['); - }; - - if (char4 == '~') { - return switch (char3) { - '1' => Key.home, - '3' => Key.delete, - '4' => Key.end, - '5' => Key.page_up, - '6' => Key.page_down, - '7' => Key.home, - '8' => Key.end, - else => { - std.log.err("Unknown terminal sequence '^[[{c}~'", .{char3}); - try self.key_buffer.append(Key.char('~')); - try self.key_buffer.append(Key.char(char3)); - return Key.meta('['); - }, - }; - } else if (char4 == ';' and char3 == '1') { - const char5 = (try readByte(std_in)) orelse { - std.log.err("Unknown terminal sequence '^[[1;'", .{}); - try self.key_buffer.append(Key.char(';')); - try self.key_buffer.append(Key.char('1')); - return Key.meta('['); - }; - - if (char5 == '5') { - const char6 = (try readByte(std_in)) orelse { - std.log.err("Unknown terminal sequence '^[[1;5'", .{}); - try self.key_buffer.append(Key.char('5')); - try self.key_buffer.append(Key.char(';')); - try self.key_buffer.append(Key.char('1')); - return Key.meta('['); - }; - - return switch (char6) { - 'A' => Key.ctrl_up, - 'B' => Key.ctrl_down, - 'C' => Key.ctrl_right, - 'D' => Key.ctrl_left, - 'F' => Key.ctrl_end, - 'H' => Key.ctrl_home, - else => { - std.log.err("Unknown terminal sequence '^[[1;5{c}'", .{char6}); - try self.key_buffer.append(Key.char(char6)); - try self.key_buffer.append(Key.char('5')); - try self.key_buffer.append(Key.char(';')); - try self.key_buffer.append(Key.char('1')); - return Key.meta('['); - }, - }; - } else { - std.log.err("Unknown terminal sequence '^[[1;{c}'", .{char5}); - try self.key_buffer.append(Key.char(char5)); - try self.key_buffer.append(Key.char(';')); - try self.key_buffer.append(Key.char('1')); - return Key.meta('['); - } - } else { - std.log.err("Unknown terminal sequence '^[[{c}{c}'", .{char3, char4}); - try self.key_buffer.append(Key.char(char4)); - try self.key_buffer.append(Key.char(char3)); - return Key.meta('['); - } - } else { - return switch (char3) { - 'A' => Key.up, - 'B' => Key.down, - 'C' => Key.right, - 'D' => Key.left, - 'F' => Key.end, - 'H' => Key.home, - else => { - std.log.err("Unknown terminal sequence '^[[{c}'", .{char3}); - try self.key_buffer.append(Key.char(char3)); - return Key.meta('['); - }, - }; - } - } else if (char2 == 'O') { - const char3 = (try readByte(std_in)) orelse { return Key.meta('O'); }; - return switch (char3) { - 'F' => Key.end, - 'H' => Key.home, - else => { - std.log.err("Unknown terminal sequence '^[O{c}'", .{char3}); - try self.key_buffer.append(Key.char(char3)); - return Key.meta('O'); - }, - }; - } else { - return Key.meta(char2); - } -} -// C- = ^[[5;5~ -// C- = ^[[6;5 - -// S- adds ;2, M- adds ;3, S-M- adds ;4, C- adds ;5, S-C- adds ;6, M-C- adds ;7, S-M-C- adds ;8 diff --git a/src/KeyReader.zig b/src/KeyReader.zig new file mode 100644 index 0000000..3aac750 --- /dev/null +++ b/src/KeyReader.zig @@ -0,0 +1,221 @@ +const es = @import("root"); +const std = @import("std"); + +const Allocator = std.mem.Allocator; +const ArrayList = std.ArrayList; +const File = std.fs.File; +const Key = es.Key; +const KeyReader = @This(); + +pub const Error = Allocator.Error || File.Reader.Error; + +allocator: Allocator, +key_buf: ArrayList(Key), + +pub fn init(allocator: Allocator) KeyReader { + return .{ + .allocator = allocator, + .key_buf = ArrayList(Key).init(allocator), + }; +} + +pub fn deinit(self: KeyReader) void { + self.key_buf.deinit(); +} + +pub fn readKey(self: *KeyReader) Error!Key { + if (self.key_buf.items.len > 0) { + return self.key_buf.pop(); + } + + const reader = std.io.getStdIn().reader(); + const char = try readByteBlocking(reader); + if (char == '\x1b') { + return self.readMetaKey(reader); + } else if (char == '\x9b') { + return self.readControlSequence(reader); + } else { + return Key.char(char); + } +} + +fn chooseEscapeKey(final_char: u8) ?Key { + return switch (final_char) { + 'A' => Key.up, + 'B' => Key.down, + 'C' => Key.right, + 'D' => Key.left, + 'F' => Key.end, + 'H' => Key.home, + else => null, + }; +} + +fn chooseTildeKey(num: usize) ?Key { + return switch (num) { + 1 => Key.home, + 2 => Key.insert, + 3 => Key.delete, + 4 => Key.end, + 5 => Key.page_up, + 6 => Key.page_down, + 7 => Key.home, + 8 => Key.end, + else => null, + }; +} + +fn modKey(key: Key, num: usize) ?Key { + return switch (num) { + 2 => Key.shift(key), + 3 => Key.meta(key), + 4 => Key.shift(Key.meta(key)), + 5 => Key.ctrl(key), + 6 => Key.shift(Key.ctrl(key)), + 7 => Key.meta(Key.ctrl(key)), + 8 => Key.shift(Key.meta(Key.ctrl(key))), + else => null, + }; +} + +fn readByte(reader: File.Reader) Error!?u8 { + return reader.readByte() catch |err| switch (err) { + error.WouldBlock => null, + error.EndOfStream => null, + else => return @errSetCast(File.Reader.Error, err), + }; +} + +// TODO: async +fn readByteBlocking(reader: File.Reader) Error!u8 { + var char = try readByte(reader); + while (char == null) { + std.os.sched_yield() catch {}; // :) + char = try readByte(reader); + } + + return char.?; +} + +fn readControlSequence(self: *KeyReader, reader: File.Reader) !Key { + var parameters = ArrayList(u8).init(self.allocator); + defer parameters.deinit(); + + var char = try readByte(reader); + while (char != null and char.? > 0x30 and char.? < 0x3F) : (char = try readByte(reader)) { + try parameters.append(char.?); + } + + var intermediates = ArrayList(u8).init(self.allocator); + defer intermediates.deinit(); + + while (char != null and char.? > 0x20 and char.? < 0x2F) : (char = try readByte(reader)) { + try intermediates.append(char.?); + } + + if (char) |final| { + if (final == '~' and intermediates.items.len == 0) { + if (try splitParameters(self.allocator, parameters.items)) |parameter_list| { + defer self.allocator.free(parameter_list); + if (chooseTildeKey(parameter_list[0])) |key| { + if (parameter_list.len == 1) { + return key; + } else if (parameter_list.len == 2) { + if (modKey(key, parameter_list[1])) |mod_key| { + return mod_key; + } + } + } + } + } else if (intermediates.items.len == 0) { + if (chooseEscapeKey(final)) |key| { + if (try splitParameters(self.allocator, parameters.items)) |parameter_list| { + defer self.allocator.free(parameter_list); + if (parameter_list.len == 0) { + return key; + } else if (parameter_list.len == 1) { + var count = std.math.max(1, parameter_list[0]) - 1; + try self.key_buf.ensureUnusedCapacity(count); + while (count > 0) : (count -= 1) { + self.key_buf.appendAssumeCapacity(key); + } + + return key; + } else if (parameter_list.len == 2) { + var count = std.math.max(1, parameter_list[0]) - 1; + if (modKey(key, parameter_list[1])) |mod_key| { + try self.key_buf.ensureUnusedCapacity(count); + while (count > 0) : (count -= 1) { + self.key_buf.appendAssumeCapacity(mod_key); + } + + return mod_key; + } + } + } + } + } + + std.log.err( + "Unknown terminal sequence '^[[{s}|{s}|{c}'", + .{parameters.items, intermediates.items, final}, + ); + + try self.key_buf.ensureUnusedCapacity(parameters.items.len + intermediates.items.len + 1); + self.key_buf.appendAssumeCapacity(Key.char(final)); + } else if (parameters.items.len == 0 and intermediates.items.len == 0) { + return Key.meta('['); + } else { + std.log.err( + "Unknown terminal sequence '^[[{s}|{s}'", + .{parameters.items, intermediates.items}, + ); + } + + try self.key_buf.ensureUnusedCapacity(parameters.items.len + intermediates.items.len); + + while (intermediates.items.len > 0) { + self.key_buf.appendAssumeCapacity(Key.char(intermediates.pop())); + } + + while (parameters.items.len > 0) { + self.key_buf.appendAssumeCapacity(Key.char(parameters.pop())); + } + + return Key.meta('['); +} + +fn readMetaKey(self: *KeyReader, reader: File.Reader) Error!Key { + if (try readByte(reader)) |char| { + if (char == '[') { + return self.readControlSequence(reader); + } else { + return Key.meta(char); + } + } else { + return Key.escape; + } +} + +fn splitParameters(allocator: Allocator, parameters_string: []const u8) Allocator.Error!?[]usize { + var parameters = try ArrayList(usize).initCapacity(allocator, parameters_string.len / 2); + defer parameters.deinit(); + + var it = std.mem.split(u8, parameters_string, ";"); + while (it.next()) |parameter_string| { + if (parameter_string.len == 0) { + // TODO: Default value + try parameters.append(1); + } else { + const parameter = std.fmt.parseUnsigned( + usize, + parameter_string, + 10, + ) catch { return null; }; + + try parameters.append(parameter); + } + } + + return parameters.toOwnedSlice(); +} diff --git a/src/key.zig b/src/key.zig index 5376017..d508ddf 100644 --- a/src/key.zig +++ b/src/key.zig @@ -1,34 +1,27 @@ const std = @import("std"); pub const Key = enum(u16) { + tab = 0x09, return_ = 0x0d, escape = 0x1b, space = 0x20, backspace = 0x7f, max_char = 0xff, - meta_nil = 0x100, - meta_max_char = 0x1ff, - - left, - right, up, down, - home, + right, + left, end, + home, + insert, delete, page_up, page_down, - ctrl_left, - ctrl_right, - ctrl_up, - ctrl_down, - ctrl_home, - ctrl_end, - // ctrl_delete, - // ctrl_page_up, - // ctrl_page_down, + mod_shft = 0x1000, + mod_meta = 0x2000, + mod_ctrl = 0x4000, _, @@ -36,8 +29,59 @@ pub const Key = enum(u16) { return @intToEnum(Key, ch); } - pub fn ctrl(ch: u8) Key { - return @intToEnum(Key, ch & 0x1f); + pub fn shift(k: anytype) Key { + return Key.mod(.mod_shft, Key.ify(k)); + } + + pub fn meta(k: anytype) Key { + return Key.mod(.mod_meta, Key.ify(k)); + } + + pub fn ctrl(k: anytype) Key { + return Key.mod(.mod_ctrl, Key.ify(k)); + } + + /// Key.ify == Keyify :) + fn ify(k: anytype) Key { + return switch (@TypeOf(k)) { + comptime_int, u8 => Key.char(k), + Key => k, + else => unreachable, + }; + } + + fn mod(comptime modifier: Key, k: Key) Key { + comptime std.debug.assert( + modifier == .mod_shft + or modifier == .mod_meta + or modifier == .mod_ctrl + ); + + const shft_int = @enumToInt(Key.mod_shft); + const meta_int = @enumToInt(Key.mod_meta); + const ctrl_int = @enumToInt(Key.mod_ctrl); + + const max_char_int = @enumToInt(Key.max_char); + + const mod_int = @enumToInt(modifier); + const k_int = @enumToInt(k); + if (k_int & mod_int == mod_int) { + return k; + } + + const k_origmod = k_int & (shft_int | meta_int | ctrl_int); + const k_nomod = k_int & ~k_origmod; + if (k_nomod <= max_char_int) { + // Appending S- to a character is not smart + std.debug.assert(modifier != .mod_shft); + return switch (modifier) { + .mod_meta => @intToEnum(Key, k_int | meta_int), + .mod_ctrl => @intToEnum(Key, k_origmod | (k_nomod & 0x1f)), + else => unreachable, + }; + } else { + return @intToEnum(Key, k_int | mod_int); + } } pub fn format( @@ -51,70 +95,80 @@ pub const Key = enum(u16) { }; return switch (key) { + .tab => std.fmt.formatBuf("", options, writer), .return_ => std.fmt.formatBuf("", options, writer), .escape => std.fmt.formatBuf("", options, writer), .space => std.fmt.formatBuf("", options, writer), .backspace => std.fmt.formatBuf("", options, writer), .max_char => key.formatGeneric(options, writer), - .meta_nil, .meta_max_char => key.formatGeneric(options, writer), - - .left => std.fmt.formatBuf("", options, writer), - .right => std.fmt.formatBuf("", options, writer), .up => std.fmt.formatBuf("", options, writer), .down => std.fmt.formatBuf("", options, writer), - .home => std.fmt.formatBuf("", options, writer), + .right => std.fmt.formatBuf("", options, writer), + .left => std.fmt.formatBuf("", options, writer), .end => std.fmt.formatBuf("", options, writer), + .home => std.fmt.formatBuf("", options, writer), + .insert => std.fmt.formatBuf("", options, writer), .delete => std.fmt.formatBuf("", options, writer), .page_up => std.fmt.formatBuf("", options, writer), .page_down => std.fmt.formatBuf("", options, writer), - .ctrl_left => std.fmt.formatBuf("C-", options, writer), - .ctrl_right => std.fmt.formatBuf("C-", options, writer), - .ctrl_up => std.fmt.formatBuf("C-", options, writer), - .ctrl_down => std.fmt.formatBuf("C-", options, writer), - .ctrl_home => std.fmt.formatBuf("C-", options, writer), - .ctrl_end => std.fmt.formatBuf("C-", options, writer), - // ctrl_delete - // ctrl_page_up - // ctrl_page_down - + .mod_shft, .mod_meta, .mod_ctrl => key.formatGeneric(options, writer), _ => key.formatGeneric(options, writer), }; } - pub fn meta(ch: u8) Key { - return @intToEnum(Key, ch + @enumToInt(Key.meta_nil)); - } - - pub fn metaCtrl(ch: u8) Key { - return @intToEnum(Key, (ch & 0x1f) + @enumToInt(Key.meta_nil)); - } - fn formatGeneric( key: Key, options: std.fmt.FormatOptions, writer: anytype, ) @TypeOf(writer).Error!void { + const shft_int = @enumToInt(Key.mod_shft); + const meta_int = @enumToInt(Key.mod_meta); + const ctrl_int = @enumToInt(Key.mod_ctrl); + const key_int = @enumToInt(key); - if (key_int < @enumToInt(Key.space)) { - const ch = std.ascii.toLower(@intCast(u8, key_int + 0x40)); - const buf = [_]u8{ 'C', '-', ch }; - return std.fmt.formatBuf(&buf, options, writer); - } else if (key_int < @enumToInt(Key.meta_nil)) { - const buf = [_]u8{@intCast(u8, key_int)}; - // This should be printed as C-? or , it's dealt with in - // format() - std.debug.assert(buf[0] != '\x7F'); - return std.fmt.formatBuf(&buf, options, writer); - } else if (key_int <= @enumToInt(Key.meta_max_char)) { + if (key_int & shft_int == shft_int) { + try std.fmt.formatBuf("S-", options, writer); + return Key.format( + @intToEnum(Key, key_int & ~shft_int), + "", + options, + writer, + ); + } else if (key_int & meta_int == meta_int) { try std.fmt.formatBuf("M-", options, writer); return Key.format( - @intToEnum(Key, key_int - @enumToInt(Key.meta_nil)), + @intToEnum(Key, key_int & ~meta_int), + "", + options, + writer, + ); + } else if (key_int & ctrl_int == ctrl_int) { + try std.fmt.formatBuf("C-", options, writer); + return Key.format( + @intToEnum(Key, key_int & ~ctrl_int), "", options, writer, ); + } else if (key_int < 0x20) { + try std.fmt.formatBuf("C-", options, writer); + return Key.format( + Key.char(@intCast(u8, key_int + 0x40)), + "", + options, + writer + ); + } else if (key_int < 0x100) { + const ch = @intCast(u8, key_int); + if (std.ascii.isGraph(ch)) { + return writer.writeByte(ch); + } else { + try writer.writeAll("<\\x"); + try std.fmt.formatIntValue(ch, "X", options, writer); + return writer.writeAll(">"); + } } else { unreachable; } diff --git a/src/key_state.zig b/src/key_state.zig index ce20071..2cb9988 100644 --- a/src/key_state.zig +++ b/src/key_state.zig @@ -19,7 +19,7 @@ pub const Error = error{ std.os.RealPathError; pub const KeyState = fn (*Editor, *Buffer, Key) Error!void; -pub fn mgState(editor: *Editor, buf: *Buffer, key: Key) Error!void { +fn mgState(editor: *Editor, buf: *Buffer, key: Key) Error!void { editor.current_state = defaultState; editor.clearStatusMessage(); @@ -34,7 +34,23 @@ pub fn mgState(editor: *Editor, buf: *Buffer, key: Key) Error!void { } } -pub fn cxState(editor: *Editor, buf: *Buffer, key: Key) Error!void { +fn mOState(editor: *Editor, buf: *Buffer, key: Key) Error!void { + editor.current_state = defaultState; + editor.clearStatusMessage(); + + switch (key) { + // ========== <*> ========== + Key.char('F') => buf.moveEndOfLine(), + Key.char('H') => buf.moveBeginningOfLine(), + + else => { + std.log.debug("Unknown chord: M-O {}", .{key}); + try editor.setStatusMessage("Unknown chord: M-O {}", .{key}); + }, + } +} + +fn cxState(editor: *Editor, buf: *Buffer, key: Key) Error!void { editor.current_state = defaultState; editor.clearStatusMessage(); @@ -59,13 +75,14 @@ pub fn cxState(editor: *Editor, buf: *Buffer, key: Key) Error!void { pub fn defaultState(editor: *Editor, buf: *Buffer, key: Key) Error!void { switch (key) { // ========== M-C-<*> ========== - Key.metaCtrl('d'), Key.backspace => try buf.backwardDeleteChar(), + Key.meta(Key.ctrl('d')), Key.backspace => try buf.backwardDeleteChar(), // ========== M-<*> ========== Key.meta('g') => { editor.current_state = mgState; try editor.setStatusMessage("M-g-", .{}); }, + Key.meta('O') => editor.current_state = mOState, Key.meta('v'), Key.page_up => buf.pageUp(editor.screenrows), // ========== C-<*> ========== diff --git a/src/main.zig b/src/main.zig index eae3b8b..176aadd 100644 --- a/src/main.zig +++ b/src/main.zig @@ -4,8 +4,9 @@ pub const Config = @import("Config.zig"); pub const Editor = @import("Editor.zig"); pub const files = @import("files.zig"); pub const Highlight = @import("highlight.zig").Highlight; -pub const key_state = @import("key_state.zig"); pub const Key = @import("key.zig").Key; +pub const KeyReader = @import("KeyReader.zig"); +pub const key_state = @import("key_state.zig"); pub const RawMode = @import("RawMode.zig"); pub const Row = @import("Row.zig"); pub const search = @import("search.zig").search; -- cgit v1.2.3