├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon ├── examples ├── example.c └── example.zig ├── include └── linenoise.h └── src ├── c.zig ├── history.zig ├── main.zig ├── state.zig ├── term.zig └── unicode.zig /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | os: [ubuntu-latest, macos-latest] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: mlugg/setup-zig@v1 14 | with: 15 | version: '0.14.0' 16 | - name: Build 17 | run: zig build 18 | fmt: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: mlugg/setup-zig@v1 23 | with: 24 | version: '0.14.0' 25 | - name: zig fmt 26 | run: zig fmt --check src/*.zig 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache/ 2 | zig-out/ 3 | example 4 | example.o 5 | history.txt 6 | /.vs 7 | /.vscode 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Joachim Schmidt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # linenoize 2 | 3 | A port of [linenoise](https://github.com/antirez/linenoise) to zig 4 | aiming to be a simple readline for command-line applications written 5 | in zig. It is written in pure zig and doesn't require 6 | libc. `linenoize` works with the latest stable zig version (0.14.0). 7 | 8 | In addition to being a full-fledged zig library, `linenoize` also 9 | serves as a drop-in replacement for linenoise. As a proof of concept, 10 | the example application from linenoise can be built with `zig build 11 | c-example`. 12 | 13 | ## Features 14 | 15 | - Line editing 16 | - Completions 17 | - Hints 18 | - History 19 | - Multi line mode 20 | - Mask input mode 21 | 22 | ### Supported platforms 23 | 24 | - Linux 25 | - macOS 26 | - Windows 27 | 28 | ## Add linenoize to a project 29 | 30 | Add linenoize as a dependency to your project: 31 | ```bash 32 | zig fetch --save git+https://github.com/joachimschmidt557/linenoize.git#v0.1.0 33 | ``` 34 | 35 | Then add the following code to your `build.zig` file: 36 | ```zig 37 | const linenoize = b.dependency("linenoize", .{ 38 | .target = target, 39 | .optimize = optimize, 40 | }).module("linenoise"); 41 | exe.root_module.addImport("linenoize", linenoize); 42 | ``` 43 | 44 | ## Examples 45 | 46 | ### Minimal example 47 | 48 | ```zig 49 | const std = @import("std"); 50 | const Linenoise = @import("linenoise").Linenoise; 51 | 52 | pub fn main() !void { 53 | const allocator = std.heap.page_allocator; 54 | 55 | var ln = Linenoise.init(allocator); 56 | defer ln.deinit(); 57 | 58 | while (try ln.linenoise("hello> ")) |input| { 59 | defer allocator.free(input); 60 | std.debug.print("input: {s}\n", .{input}); 61 | try ln.history.add(input); 62 | } 63 | } 64 | ``` 65 | 66 | ### Example of more features 67 | 68 | ``` zig 69 | const std = @import("std"); 70 | const Allocator = std.mem.Allocator; 71 | const ArrayList = std.ArrayList; 72 | 73 | const log = std.log.scoped(.main); 74 | 75 | const Linenoise = @import("linenoise").Linenoise; 76 | 77 | fn completion(allocator: Allocator, buf: []const u8) ![]const []const u8 { 78 | if (std.mem.eql(u8, "z", buf)) { 79 | var result = ArrayList([]const u8).init(allocator); 80 | try result.append(try allocator.dupe(u8, "zig")); 81 | try result.append(try allocator.dupe(u8, "ziglang")); 82 | return result.toOwnedSlice(); 83 | } else { 84 | return &[_][]const u8{}; 85 | } 86 | } 87 | 88 | fn hints(allocator: Allocator, buf: []const u8) !?[]const u8 { 89 | if (std.mem.eql(u8, "hello", buf)) { 90 | return try allocator.dupe(u8, " World"); 91 | } else { 92 | return null; 93 | } 94 | } 95 | 96 | var debug_allocator: std.heap.DebugAllocator(.{}) = .init; 97 | 98 | pub fn main() !void { 99 | defer _ = debug_allocator.deinit(); 100 | const allocator = debug_allocator.allocator(); 101 | 102 | var ln = Linenoise.init(allocator); 103 | defer ln.deinit(); 104 | 105 | // Load history and save history later 106 | ln.history.load("history.txt") catch log.err("Failed to load history", .{}); 107 | defer ln.history.save("history.txt") catch log.err("Failed to save history", .{}); 108 | 109 | // Set up hints callback 110 | ln.hints_callback = hints; 111 | 112 | // Set up completions callback 113 | ln.completions_callback = completion; 114 | 115 | // Enable mask mode 116 | // ln.mask_mode = true; 117 | 118 | // Enable multiline mode 119 | // ln.multiline_mode = true; 120 | 121 | while (try ln.linenoise("hellö> ")) |input| { 122 | defer allocator.free(input); 123 | log.info("input: {s}", .{input}); 124 | try ln.history.add(input); 125 | } 126 | } 127 | ``` 128 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const Build = @import("std").Build; 2 | 3 | pub fn build(b: *Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const wcwidth = b.dependency("wcwidth", .{ 8 | .target = target, 9 | .optimize = optimize, 10 | }).module("wcwidth"); 11 | 12 | const linenoise = b.addModule("linenoise", .{ 13 | .root_source_file = b.path("src/main.zig"), 14 | .imports = &.{ 15 | .{ .name = "wcwidth", .module = wcwidth }, 16 | }, 17 | .target = target, 18 | .optimize = optimize, 19 | }); 20 | 21 | // Static library 22 | const lib = b.addStaticLibrary(.{ 23 | .name = "linenoise", 24 | .root_module = b.createModule(.{ 25 | .root_source_file = b.path("src/c.zig"), 26 | .target = target, 27 | .optimize = optimize, 28 | }), 29 | }); 30 | lib.root_module.addImport("wcwidth", wcwidth); 31 | lib.linkLibC(); 32 | b.installArtifact(lib); 33 | 34 | // Tests 35 | const main_tests = b.addTest(.{ 36 | .root_module = linenoise, 37 | }); 38 | 39 | const run_main_tests = b.addRunArtifact(main_tests); 40 | 41 | const test_step = b.step("test", "Run library tests"); 42 | test_step.dependOn(&run_main_tests.step); 43 | 44 | // Zig example 45 | var example = b.addExecutable(.{ 46 | .name = "example", 47 | .root_module = b.createModule(.{ 48 | .root_source_file = b.path("examples/example.zig"), 49 | .target = target, 50 | .optimize = optimize, 51 | }), 52 | }); 53 | example.root_module.addImport("linenoise", linenoise); 54 | 55 | var example_run = b.addRunArtifact(example); 56 | 57 | const example_step = b.step("example", "Run example"); 58 | example_step.dependOn(&example_run.step); 59 | 60 | // C example 61 | var c_example = b.addExecutable(.{ 62 | .name = "example", 63 | .root_module = b.createModule(.{ 64 | .target = target, 65 | .optimize = optimize, 66 | }), 67 | }); 68 | c_example.root_module.addCSourceFile(.{ .file = b.path("examples/example.c") }); 69 | c_example.addIncludePath(b.path("include")); 70 | c_example.linkLibC(); 71 | c_example.linkLibrary(lib); 72 | 73 | var c_example_run = b.addRunArtifact(c_example); 74 | 75 | const c_example_step = b.step("c-example", "Run C example"); 76 | c_example_step.dependOn(&c_example_run.step); 77 | c_example_step.dependOn(&lib.step); 78 | } 79 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .linenoize, 3 | .fingerprint = 0xfa03aea2f0cab127, 4 | .version = "0.1.1", 5 | .dependencies = .{ 6 | .wcwidth = .{ 7 | .url = "git+https://github.com/joachimschmidt557/zig-wcwidth?ref=v0.1.0#4f5c8efa838da57c9e1b14506138936964835999", 8 | .hash = "wcwidth-0.1.0-A4Aa6obmAAC40epfTYwhsdITDO3M6dHEWf6C0jeGMWrV", 9 | }, 10 | }, 11 | .paths = .{ 12 | "LICENSE", 13 | "README.md", 14 | "build.zig", 15 | "build.zig.zon", 16 | "examples/", 17 | "includes/", 18 | "src/", 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /examples/example.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "linenoise.h" 5 | 6 | 7 | void completion(const char *buf, linenoiseCompletions *lc) { 8 | if (buf[0] == 'h') { 9 | linenoiseAddCompletion(lc,"hello"); 10 | linenoiseAddCompletion(lc,"hello there"); 11 | } 12 | } 13 | 14 | char *hints(const char *buf, int *color, int *bold) { 15 | if (!strcasecmp(buf,"hello")) { 16 | *color = 35; 17 | *bold = 0; 18 | return " World"; 19 | } 20 | return NULL; 21 | } 22 | 23 | int main(int argc, char **argv) { 24 | char *line; 25 | char *prgname = argv[0]; 26 | 27 | /* Parse options, with --multiline we enable multi line editing. */ 28 | while(argc > 1) { 29 | argc--; 30 | argv++; 31 | if (!strcmp(*argv,"--multiline")) { 32 | linenoiseSetMultiLine(1); 33 | printf("Multi-line mode enabled.\n"); 34 | } else if (!strcmp(*argv,"--keycodes")) { 35 | linenoisePrintKeyCodes(); 36 | exit(0); 37 | } else { 38 | fprintf(stderr, "Usage: %s [--multiline] [--keycodes]\n", prgname); 39 | exit(1); 40 | } 41 | } 42 | 43 | /* Set the completion callback. This will be called every time the 44 | * user uses the key. */ 45 | linenoiseSetCompletionCallback(completion); 46 | linenoiseSetHintsCallback(hints); 47 | 48 | /* Load history from file. The history file is just a plain text file 49 | * where entries are separated by newlines. */ 50 | linenoiseHistoryLoad("history.txt"); /* Load the history at startup */ 51 | 52 | /* Now this is the main loop of the typical linenoise-based application. 53 | * The call to linenoise() will block as long as the user types something 54 | * and presses enter. 55 | * 56 | * The typed string is returned as a malloc() allocated string by 57 | * linenoise, so the user needs to free() it. */ 58 | 59 | while((line = linenoise("hello> ")) != NULL) { 60 | /* Do something with the string. */ 61 | if (line[0] != '\0' && line[0] != '/') { 62 | printf("echo: '%s'\n", line); 63 | linenoiseHistoryAdd(line); /* Add to the history. */ 64 | linenoiseHistorySave("history.txt"); /* Save the history on disk. */ 65 | } else if (!strncmp(line,"/historylen",11)) { 66 | /* The "/historylen" command will change the history len. */ 67 | int len = atoi(line+11); 68 | linenoiseHistorySetMaxLen(len); 69 | } else if (!strncmp(line, "/mask", 5)) { 70 | linenoiseMaskModeEnable(); 71 | } else if (!strncmp(line, "/unmask", 7)) { 72 | linenoiseMaskModeDisable(); 73 | } else if (line[0] == '/') { 74 | printf("Unreconized command: %s\n", line); 75 | } 76 | free(line); 77 | } 78 | return 0; 79 | } 80 | -------------------------------------------------------------------------------- /examples/example.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const ArrayList = std.ArrayList; 4 | 5 | const log = std.log.scoped(.main); 6 | 7 | const Linenoise = @import("linenoise").Linenoise; 8 | 9 | fn completion(allocator: Allocator, buf: []const u8) ![]const []const u8 { 10 | if (std.mem.eql(u8, "z", buf)) { 11 | var result = ArrayList([]const u8).init(allocator); 12 | try result.append(try allocator.dupe(u8, "zig")); 13 | try result.append(try allocator.dupe(u8, "ziglang")); 14 | return result.toOwnedSlice(); 15 | } else { 16 | return &[_][]const u8{}; 17 | } 18 | } 19 | 20 | fn hints(allocator: Allocator, buf: []const u8) !?[]const u8 { 21 | if (std.mem.eql(u8, "hello", buf)) { 22 | return try allocator.dupe(u8, " World"); 23 | } else { 24 | return null; 25 | } 26 | } 27 | 28 | var debug_allocator: std.heap.DebugAllocator(.{}) = .init; 29 | 30 | pub fn main() !void { 31 | defer _ = debug_allocator.deinit(); 32 | const allocator = debug_allocator.allocator(); 33 | 34 | var ln = Linenoise.init(allocator); 35 | defer ln.deinit(); 36 | 37 | // Load history and save history later 38 | ln.history.load("history.txt") catch log.err("Failed to load history", .{}); 39 | defer ln.history.save("history.txt") catch log.err("Failed to save history", .{}); 40 | 41 | // Set up hints callback 42 | ln.hints_callback = hints; 43 | 44 | // Set up completions callback 45 | ln.completions_callback = completion; 46 | 47 | // Enable mask mode 48 | // ln.mask_mode = true; 49 | 50 | // Enable multiline mode 51 | // ln.multiline_mode = true; 52 | 53 | while (try ln.linenoise("hellö> ")) |input| { 54 | defer allocator.free(input); 55 | log.info("input: {s}", .{input}); 56 | try ln.history.add(input); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /include/linenoise.h: -------------------------------------------------------------------------------- 1 | /* linenoise.h -- VERSION 1.0 2 | * 3 | * Guerrilla line editing library against the idea that a line editing lib 4 | * needs to be 20,000 lines of C code. 5 | * 6 | * See linenoise.c for more information. 7 | * 8 | * ------------------------------------------------------------------------ 9 | * 10 | * Copyright (c) 2010-2014, Salvatore Sanfilippo 11 | * Copyright (c) 2010-2013, Pieter Noordhuis 12 | * 13 | * All rights reserved. 14 | * 15 | * Redistribution and use in source and binary forms, with or without 16 | * modification, are permitted provided that the following conditions are 17 | * met: 18 | * 19 | * * Redistributions of source code must retain the above copyright 20 | * notice, this list of conditions and the following disclaimer. 21 | * 22 | * * Redistributions in binary form must reproduce the above copyright 23 | * notice, this list of conditions and the following disclaimer in the 24 | * documentation and/or other materials provided with the distribution. 25 | * 26 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 27 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 28 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 29 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 30 | * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 31 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 32 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 33 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 34 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 35 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 36 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 37 | */ 38 | 39 | #ifndef __LINENOISE_H 40 | #define __LINENOISE_H 41 | 42 | #ifdef __cplusplus 43 | extern "C" { 44 | #endif 45 | 46 | typedef struct linenoiseCompletions { 47 | size_t len; 48 | char **cvec; 49 | } linenoiseCompletions; 50 | 51 | typedef void(linenoiseCompletionCallback)(const char *, linenoiseCompletions *); 52 | typedef char*(linenoiseHintsCallback)(const char *, int *color, int *bold); 53 | typedef void(linenoiseFreeHintsCallback)(void *); 54 | void linenoiseSetCompletionCallback(linenoiseCompletionCallback *); 55 | void linenoiseSetHintsCallback(linenoiseHintsCallback *); 56 | void linenoiseSetFreeHintsCallback(linenoiseFreeHintsCallback *); 57 | void linenoiseAddCompletion(linenoiseCompletions *, const char *); 58 | 59 | char *linenoise(const char *prompt); 60 | void linenoiseFree(void *ptr); 61 | int linenoiseHistoryAdd(const char *line); 62 | int linenoiseHistorySetMaxLen(int len); 63 | int linenoiseHistorySave(const char *filename); 64 | int linenoiseHistoryLoad(const char *filename); 65 | void linenoiseClearScreen(void); 66 | void linenoiseSetMultiLine(int ml); 67 | void linenoisePrintKeyCodes(void); 68 | void linenoiseMaskModeEnable(void); 69 | void linenoiseMaskModeDisable(void); 70 | 71 | #ifdef __cplusplus 72 | } 73 | #endif 74 | 75 | #endif /* __LINENOISE_H */ 76 | -------------------------------------------------------------------------------- /src/c.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const fmt = std.fmt; 3 | const mem = std.mem; 4 | const Allocator = mem.Allocator; 5 | 6 | const Linenoise = @import("main.zig").Linenoise; 7 | const term = @import("term.zig"); 8 | 9 | const global_allocator = std.heap.c_allocator; 10 | var global_linenoise: ?Linenoise = null; 11 | 12 | var c_completion_callback: ?linenoiseCompletionCallback = null; 13 | var c_hints_callback: ?linenoiseHintsCallback = null; 14 | var c_free_hints_callback: ?linenoiseFreeHintsCallback = null; 15 | 16 | const LinenoiseCompletions = extern struct { 17 | len: usize, 18 | cvec: ?[*][*:0]u8, 19 | 20 | pub fn free(self: *LinenoiseCompletions) void { 21 | if (self.cvec) |raw_completions| { 22 | const len: usize = @intCast(self.len); 23 | for (raw_completions[0..len]) |x| global_allocator.free(mem.span(x)); 24 | global_allocator.free(raw_completions[0..len]); 25 | } 26 | } 27 | }; 28 | 29 | const linenoiseCompletionCallback = *const fn ([*:0]const u8, *LinenoiseCompletions) callconv(.C) void; 30 | const linenoiseHintsCallback = *const fn ([*:0]const u8, *c_int, *c_int) callconv(.C) ?[*:0]u8; 31 | const linenoiseFreeHintsCallback = *const fn (*anyopaque) callconv(.C) void; 32 | 33 | export fn linenoiseSetCompletionCallback(fun: linenoiseCompletionCallback) void { 34 | c_completion_callback = fun; 35 | if (global_linenoise == null) global_linenoise = Linenoise.init(global_allocator); 36 | global_linenoise.?.completions_callback = completionsCallback; 37 | } 38 | 39 | export fn linenoiseSetHintsCallback(fun: linenoiseHintsCallback) void { 40 | c_hints_callback = fun; 41 | if (global_linenoise == null) global_linenoise = Linenoise.init(global_allocator); 42 | global_linenoise.?.hints_callback = hintsCallback; 43 | } 44 | 45 | export fn linenoiseSetFreeHintsCallback(fun: linenoiseFreeHintsCallback) void { 46 | c_free_hints_callback = fun; 47 | } 48 | 49 | export fn linenoiseAddCompletion(lc: *LinenoiseCompletions, str: [*:0]const u8) void { 50 | const dupe = global_allocator.dupeZ(u8, mem.span(str)) catch return; 51 | 52 | var completions: std.ArrayList([*:0]u8) = undefined; 53 | if (lc.cvec) |raw_completions| { 54 | completions = std.ArrayList([*:0]u8).fromOwnedSlice(global_allocator, raw_completions[0..lc.len]); 55 | } else { 56 | completions = std.ArrayList([*:0]u8).init(global_allocator); 57 | } 58 | 59 | completions.append(dupe) catch return; 60 | const slice = completions.toOwnedSlice() catch return; 61 | lc.cvec = slice.ptr; 62 | lc.len += 1; 63 | } 64 | 65 | fn completionsCallback(allocator: Allocator, line: []const u8) ![]const []const u8 { 66 | if (c_completion_callback) |cCompletionCallback| { 67 | const lineZ = try allocator.dupeZ(u8, line); 68 | defer allocator.free(lineZ); 69 | 70 | var lc: LinenoiseCompletions = .{ 71 | .len = 0, 72 | .cvec = null, 73 | }; 74 | cCompletionCallback(lineZ, &lc); 75 | 76 | if (lc.cvec) |raw_completions| { 77 | defer lc.free(); 78 | 79 | const completions = try allocator.alloc([]const u8, lc.len); 80 | for (completions, 0..) |*x, i| { 81 | x.* = try allocator.dupe(u8, mem.span(raw_completions[i])); 82 | } 83 | 84 | return completions; 85 | } 86 | } 87 | 88 | return &[_][]const u8{}; 89 | } 90 | 91 | fn hintsCallback(allocator: Allocator, line: []const u8) !?[]const u8 { 92 | if (c_hints_callback) |cHintsCallback| { 93 | const lineZ = try allocator.dupeZ(u8, line); 94 | defer allocator.free(lineZ); 95 | 96 | var color: c_int = -1; 97 | var bold: c_int = 0; 98 | const maybe_hint = cHintsCallback(lineZ, &color, &bold); 99 | if (maybe_hint) |hintZ| { 100 | defer { 101 | if (c_free_hints_callback) |cFreeHintsCallback| { 102 | cFreeHintsCallback(hintZ); 103 | } 104 | } 105 | 106 | const hint = mem.span(hintZ); 107 | if (bold == 1 and color == -1) { 108 | color = 37; 109 | } 110 | 111 | return try fmt.allocPrint(allocator, "\x1B[{};{};49m{s}\x1B[0m", .{ 112 | bold, 113 | color, 114 | hint, 115 | }); 116 | } 117 | } 118 | 119 | return null; 120 | } 121 | 122 | export fn linenoise(prompt: [*:0]const u8) ?[*:0]u8 { 123 | if (global_linenoise == null) global_linenoise = Linenoise.init(global_allocator); 124 | const result = global_linenoise.?.linenoise(mem.span(prompt)) catch return null; 125 | if (result) |line| { 126 | defer global_allocator.free(line); 127 | return global_allocator.dupeZ(u8, line) catch return null; 128 | } else return null; 129 | } 130 | 131 | export fn linenoiseFree(ptr: *anyopaque) void { 132 | global_allocator.free(mem.span(@as([*:0]const u8, @ptrCast(ptr)))); 133 | } 134 | 135 | export fn linenoiseHistoryAdd(line: [*:0]const u8) c_int { 136 | if (global_linenoise == null) global_linenoise = Linenoise.init(global_allocator); 137 | global_linenoise.?.history.add(mem.span(line)) catch return -1; 138 | return 0; 139 | } 140 | 141 | export fn linenoiseHistorySetMaxLen(len: c_int) c_int { 142 | if (global_linenoise == null) global_linenoise = Linenoise.init(global_allocator); 143 | global_linenoise.?.history.setMaxLen(@intCast(len)); 144 | return 0; 145 | } 146 | 147 | export fn linenoiseHistorySave(filename: [*:0]const u8) c_int { 148 | if (global_linenoise == null) global_linenoise = Linenoise.init(global_allocator); 149 | global_linenoise.?.history.save(mem.span(filename)) catch return -1; 150 | return 0; 151 | } 152 | 153 | export fn linenoiseHistoryLoad(filename: [*:0]const u8) c_int { 154 | if (global_linenoise == null) global_linenoise = Linenoise.init(global_allocator); 155 | global_linenoise.?.history.load(mem.span(filename)) catch return -1; 156 | return 0; 157 | } 158 | 159 | export fn linenoiseClearScreen() void { 160 | term.clearScreen() catch return; 161 | } 162 | 163 | export fn linenoiseSetMultiLine(ml: c_int) void { 164 | if (global_linenoise == null) global_linenoise = Linenoise.init(global_allocator); 165 | global_linenoise.?.multiline_mode = ml != 0; 166 | } 167 | 168 | /// Not implemented in linenoize 169 | export fn linenoisePrintKeyCodes() void {} 170 | 171 | export fn linenoiseMaskModeEnable() void { 172 | if (global_linenoise == null) global_linenoise = Linenoise.init(global_allocator); 173 | global_linenoise.?.mask_mode = true; 174 | } 175 | 176 | export fn linenoiseMaskModeDisable() void { 177 | if (global_linenoise == null) global_linenoise = Linenoise.init(global_allocator); 178 | global_linenoise.?.mask_mode = false; 179 | } 180 | -------------------------------------------------------------------------------- /src/history.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const ArrayListUnmanaged = std.ArrayListUnmanaged; 4 | 5 | const max_line_len = 4096; 6 | 7 | pub const History = struct { 8 | allocator: Allocator, 9 | hist: ArrayListUnmanaged([]const u8) = .empty, 10 | max_len: usize = 100, 11 | current: usize = 0, 12 | 13 | const Self = @This(); 14 | 15 | /// Creates a new empty history 16 | pub fn empty(allocator: Allocator) Self { 17 | return .{ 18 | .allocator = allocator, 19 | }; 20 | } 21 | 22 | /// Deinitializes the history 23 | pub fn deinit(self: *Self) void { 24 | for (self.hist.items) |x| self.allocator.free(x); 25 | self.hist.deinit(self.allocator); 26 | } 27 | 28 | /// Ensures that at most self.max_len items are in the history 29 | fn truncate(self: *Self) void { 30 | if (self.hist.items.len > self.max_len) { 31 | const surplus = self.hist.items.len - self.max_len; 32 | for (self.hist.items[0..surplus]) |x| self.allocator.free(x); 33 | std.mem.copyForwards( 34 | []const u8, 35 | self.hist.items[0..self.max_len], 36 | self.hist.items[surplus..], 37 | ); 38 | self.hist.shrinkAndFree(self.allocator, self.max_len); 39 | } 40 | } 41 | 42 | /// Adds this line to the history. Does not take ownership of the line, but 43 | /// instead copies it 44 | pub fn add(self: *Self, line: []const u8) !void { 45 | if (self.hist.items.len < 1 or !std.mem.eql(u8, line, self.hist.items[self.hist.items.len - 1])) { 46 | try self.hist.append(self.allocator, try self.allocator.dupe(u8, line)); 47 | self.truncate(); 48 | } 49 | } 50 | 51 | /// Removes the last item (newest item) of the history 52 | pub fn pop(self: *Self) void { 53 | self.allocator.free(self.hist.pop().?); 54 | } 55 | 56 | /// Loads the history from a file 57 | pub fn load(self: *Self, path: []const u8) !void { 58 | const file = try std.fs.cwd().openFile(path, .{}); 59 | defer file.close(); 60 | 61 | const reader = file.reader(); 62 | while (reader.readUntilDelimiterAlloc(self.allocator, '\n', max_line_len)) |line| { 63 | try self.hist.append(self.allocator, line); 64 | } else |err| { 65 | switch (err) { 66 | error.EndOfStream => return, 67 | else => return err, 68 | } 69 | } 70 | 71 | self.truncate(); 72 | } 73 | 74 | /// Saves the history to a file 75 | pub fn save(self: *Self, path: []const u8) !void { 76 | const file = try std.fs.cwd().createFile(path, .{}); 77 | defer file.close(); 78 | 79 | for (self.hist.items) |line| { 80 | try file.writeAll(line); 81 | try file.writeAll("\n"); 82 | } 83 | } 84 | 85 | /// Sets the maximum number of history items. If more history 86 | /// items than len exist, this will truncate the history to the 87 | /// len most recent items. 88 | pub fn setMaxLen(self: *Self, len: usize) void { 89 | self.max_len = len; 90 | self.truncate(); 91 | } 92 | }; 93 | 94 | test "history" { 95 | var hist = History.empty(std.testing.allocator); 96 | defer hist.deinit(); 97 | 98 | try hist.add("Hello"); 99 | hist.pop(); 100 | } 101 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const ArrayList = std.ArrayList; 4 | const File = std.fs.File; 5 | 6 | const LinenoiseState = @import("state.zig").LinenoiseState; 7 | pub const History = @import("history.zig").History; 8 | const term = @import("term.zig"); 9 | const isUnsupportedTerm = term.isUnsupportedTerm; 10 | const enableRawMode = term.enableRawMode; 11 | const disableRawMode = term.disableRawMode; 12 | const getColumns = term.getColumns; 13 | 14 | pub const HintsCallback = *const fn (Allocator, []const u8) Allocator.Error!?[]const u8; 15 | pub const CompletionsCallback = *const fn (Allocator, []const u8) Allocator.Error![]const []const u8; 16 | 17 | const key_null = 0; 18 | const key_ctrl_a = 1; 19 | const key_ctrl_b = 2; 20 | const key_ctrl_c = 3; 21 | const key_ctrl_d = 4; 22 | const key_ctrl_e = 5; 23 | const key_ctrl_f = 6; 24 | const key_ctrl_h = 8; 25 | const key_tab = 9; 26 | const key_ctrl_k = 11; 27 | const key_ctrl_l = 12; 28 | const key_enter = 13; 29 | const key_ctrl_n = 14; 30 | const key_ctrl_p = 16; 31 | const key_ctrl_t = 20; 32 | const key_ctrl_u = 21; 33 | const key_ctrl_w = 23; 34 | const key_esc = 27; 35 | const key_backspace = 127; 36 | 37 | fn linenoiseEdit(ln: *Linenoise, in: File, out: File, prompt: []const u8) !?[]const u8 { 38 | var state = LinenoiseState.init(ln, in, out, prompt); 39 | defer state.buf.deinit(state.allocator); 40 | 41 | try state.ln.history.add(""); 42 | state.ln.history.current = state.ln.history.hist.items.len - 1; 43 | try state.refreshLine(); 44 | 45 | while (true) { 46 | var input_buf: [1]u8 = undefined; 47 | if ((try term.read(in, &input_buf)) < 1) return null; 48 | var c = input_buf[0]; 49 | 50 | // Browse completions before editing 51 | if (c == key_tab) { 52 | if (try state.browseCompletions()) |new_c| { 53 | c = new_c; 54 | } 55 | } 56 | 57 | switch (c) { 58 | key_null, key_tab => {}, 59 | key_ctrl_a => try state.editMoveHome(), 60 | key_ctrl_b => try state.editMoveLeft(), 61 | key_ctrl_c => return error.CtrlC, 62 | key_ctrl_d => { 63 | if (state.buf.items.len > 0) { 64 | try state.editDelete(); 65 | } else { 66 | state.ln.history.pop(); 67 | return null; 68 | } 69 | }, 70 | key_ctrl_e => try state.editMoveEnd(), 71 | key_ctrl_f => try state.editMoveRight(), 72 | key_ctrl_k => try state.editKillLineForward(), 73 | key_ctrl_l => { 74 | try term.clearScreen(); 75 | try state.refreshLine(); 76 | }, 77 | key_enter => { 78 | state.ln.history.pop(); 79 | return try ln.allocator.dupe(u8, state.buf.items); 80 | }, 81 | key_ctrl_n => try state.editHistoryNext(.next), 82 | key_ctrl_p => try state.editHistoryNext(.prev), 83 | key_ctrl_t => try state.editSwapPrev(), 84 | key_ctrl_u => try state.editKillLineBackward(), 85 | key_ctrl_w => try state.editDeletePrevWord(), 86 | key_esc => { 87 | if ((try term.read(in, &input_buf)) < 1) return null; 88 | switch (input_buf[0]) { 89 | 'b' => try state.editMoveWordStart(), 90 | 'f' => try state.editMoveWordEnd(), 91 | '[' => { 92 | if ((try term.read(in, &input_buf)) < 1) return null; 93 | switch (input_buf[0]) { 94 | '0'...'9' => |num| { 95 | if ((try in.read(&input_buf)) < 1) return null; 96 | switch (input_buf[0]) { 97 | '~' => switch (num) { 98 | '1', '7' => try state.editMoveHome(), 99 | '3' => try state.editDelete(), 100 | '4', '8' => try state.editMoveEnd(), 101 | else => {}, 102 | }, 103 | '0'...'9' => {}, // TODO: read 2-digit CSI 104 | else => {}, 105 | } 106 | }, 107 | 'A' => try state.editHistoryNext(.prev), 108 | 'B' => try state.editHistoryNext(.next), 109 | 'C' => try state.editMoveRight(), 110 | 'D' => try state.editMoveLeft(), 111 | 'H' => try state.editMoveHome(), 112 | 'F' => try state.editMoveEnd(), 113 | else => {}, 114 | } 115 | }, 116 | '0' => { 117 | if ((try term.read(in, &input_buf)) < 1) return null; 118 | switch (input_buf[0]) { 119 | 'H' => try state.editMoveHome(), 120 | 'F' => try state.editMoveEnd(), 121 | else => {}, 122 | } 123 | }, 124 | else => {}, 125 | } 126 | }, 127 | '5' => { 128 | if ((try term.read(in, &input_buf)) < 1) return null; 129 | 130 | switch (input_buf[0]) { 131 | 'C' => try state.editMoveWordEnd(), // ESC[1;5C = ctrl + right arrow 132 | 'D' => try state.editMoveWordStart(), // ESC[1;5D = ctrl + left arrow 133 | else => {}, 134 | } 135 | }, 136 | key_backspace, key_ctrl_h => try state.editBackspace(), 137 | else => { 138 | var utf8_buf: [4]u8 = undefined; 139 | const utf8_len = std.unicode.utf8ByteSequenceLength(c) catch continue; 140 | 141 | utf8_buf[0] = c; 142 | if (utf8_len > 1 and (try term.read(in, utf8_buf[1..utf8_len])) < utf8_len - 1) return null; 143 | 144 | try state.editInsert(utf8_buf[0..utf8_len]); 145 | }, 146 | } 147 | } 148 | } 149 | 150 | /// Read a line with custom line editing mechanics. This includes hints, 151 | /// completions and history 152 | fn linenoiseRaw(ln: *Linenoise, in: File, out: File, prompt: []const u8) !?[]const u8 { 153 | defer out.writeAll("\n") catch {}; 154 | 155 | const orig = try enableRawMode(in, out); 156 | defer disableRawMode(in, out, orig); 157 | 158 | return try linenoiseEdit(ln, in, out, prompt); 159 | } 160 | 161 | /// Read a line with no special features (no hints, no completions, no history) 162 | fn linenoiseNoTTY(allocator: Allocator, stdin: File) !?[]const u8 { 163 | var reader = stdin.reader(); 164 | const max_line_len = std.math.maxInt(usize); 165 | return reader.readUntilDelimiterAlloc(allocator, '\n', max_line_len) catch |e| switch (e) { 166 | error.EndOfStream => return null, 167 | else => return e, 168 | }; 169 | } 170 | 171 | pub const Linenoise = struct { 172 | allocator: Allocator, 173 | history: History, 174 | multiline_mode: bool = false, 175 | mask_mode: bool = false, 176 | is_tty: bool = false, 177 | term_supported: bool = false, 178 | hints_callback: ?HintsCallback = null, 179 | completions_callback: ?CompletionsCallback = null, 180 | 181 | const Self = @This(); 182 | 183 | /// Initialize a linenoise struct 184 | pub fn init(allocator: Allocator) Self { 185 | var self: Self = .{ 186 | .allocator = allocator, 187 | .history = History.empty(allocator), 188 | }; 189 | self.examineStdIo(); 190 | return self; 191 | } 192 | 193 | /// Free all resources occupied by this struct 194 | pub fn deinit(self: *Self) void { 195 | self.history.deinit(); 196 | } 197 | 198 | /// Re-examine (currently) stdin and environment variables to 199 | /// check if line editing and prompt printing should be 200 | /// enabled or not. 201 | pub fn examineStdIo(self: *Self) void { 202 | const stdin_file = std.io.getStdIn(); 203 | self.is_tty = stdin_file.isTty(); 204 | self.term_supported = !isUnsupportedTerm(self.allocator); 205 | } 206 | 207 | /// Reads a line from the terminal. Caller owns returned memory 208 | pub fn linenoise(self: *Self, prompt: []const u8) !?[]const u8 { 209 | const stdin_file = std.io.getStdIn(); 210 | const stdout_file = std.io.getStdOut(); 211 | 212 | if (self.is_tty and !self.term_supported) { 213 | try stdout_file.writeAll(prompt); 214 | } 215 | 216 | return if (self.is_tty and self.term_supported) 217 | try linenoiseRaw(self, stdin_file, stdout_file, prompt) 218 | else 219 | try linenoiseNoTTY(self.allocator, stdin_file); 220 | } 221 | }; 222 | 223 | test "all" { 224 | _ = @import("history.zig"); 225 | } 226 | -------------------------------------------------------------------------------- /src/state.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const ArrayList = std.ArrayList; 4 | const ArrayListUnmanaged = std.ArrayListUnmanaged; 5 | const File = std.fs.File; 6 | const bufferedWriter = std.io.bufferedWriter; 7 | const math = std.math; 8 | 9 | const Linenoise = @import("main.zig").Linenoise; 10 | const History = @import("history.zig").History; 11 | const unicode = @import("unicode.zig"); 12 | const width = unicode.width; 13 | const term = @import("term.zig"); 14 | const getColumns = term.getColumns; 15 | 16 | const key_tab = 9; 17 | const key_esc = 27; 18 | 19 | const Bias = enum { 20 | left, 21 | right, 22 | }; 23 | 24 | fn binarySearchBestEffort( 25 | comptime T: type, 26 | key: T, 27 | items: []const T, 28 | context: anytype, 29 | comptime compareFn: fn (context: @TypeOf(context), lhs: T, rhs: T) math.Order, 30 | bias: Bias, 31 | ) usize { 32 | var left: usize = 0; 33 | var right: usize = items.len; 34 | 35 | while (left < right) { 36 | // Avoid overflowing in the midpoint calculation 37 | const mid = left + (right - left) / 2; 38 | // Compare the key with the midpoint element 39 | switch (compareFn(context, key, items[mid])) { 40 | .eq => return mid, 41 | .gt => left = mid + 1, 42 | .lt => right = mid, 43 | } 44 | } 45 | 46 | // At this point, it is guaranteed that left >= right. In order 47 | // for the bias to work, we need to return the exact opposite 48 | return switch (bias) { 49 | .left => right, 50 | .right => left, 51 | }; 52 | } 53 | 54 | fn compareLeft(context: []const u8, avail_space: usize, index: usize) math.Order { 55 | const width_slice = width(context[0..index]); 56 | return math.order(avail_space, width_slice); 57 | } 58 | 59 | fn compareRight(context: []const u8, avail_space: usize, index: usize) math.Order { 60 | const width_slice = width(context[index..]); 61 | return math.order(width_slice, avail_space); 62 | } 63 | 64 | const StartOrEnd = enum { 65 | start, 66 | end, 67 | }; 68 | 69 | /// Start mode: Calculates the optimal start position such that 70 | /// buf[start..] fits in the available space taking into account 71 | /// unicode codepoint widths 72 | /// 73 | /// xxxxxxxxxxxxxxxxxxxxxxx buf 74 | /// [------------------] available_space 75 | /// ^ start 76 | /// 77 | /// End mode: Calculates the optimal end position so that buf[0..end] 78 | /// fits 79 | /// 80 | /// xxxxxxxxxxxxxxxxxxxxxxx buf 81 | /// [----------] available_space 82 | /// ^ end 83 | fn calculateStartOrEnd( 84 | allocator: Allocator, 85 | comptime mode: StartOrEnd, 86 | buf: []const u8, 87 | avail_space: usize, 88 | ) !usize { 89 | // Create a mapping from unicode codepoint indices to buf 90 | // indices 91 | var map = try std.ArrayListUnmanaged(usize).initCapacity(allocator, buf.len); 92 | defer map.deinit(allocator); 93 | 94 | var utf8 = (try std.unicode.Utf8View.init(buf)).iterator(); 95 | while (utf8.nextCodepointSlice()) |codepoint| { 96 | map.appendAssumeCapacity(@intFromPtr(codepoint.ptr) - @intFromPtr(buf.ptr)); 97 | } 98 | 99 | const codepoint_start = binarySearchBestEffort( 100 | usize, 101 | avail_space, 102 | map.items, 103 | buf, 104 | switch (mode) { 105 | .start => compareRight, 106 | .end => compareLeft, 107 | }, 108 | // When calculating start or end, if in doubt, choose a 109 | // smaller buffer as we don't want to overflow the line. For 110 | // calculating start, this means that we would rather have an 111 | // index more to the right. 112 | switch (mode) { 113 | .start => .right, 114 | .end => .left, 115 | }, 116 | ); 117 | return map.items[codepoint_start]; 118 | } 119 | 120 | pub const LinenoiseState = struct { 121 | allocator: Allocator, 122 | ln: *Linenoise, 123 | 124 | stdin: File, 125 | stdout: File, 126 | buf: ArrayListUnmanaged(u8) = .empty, 127 | prompt: []const u8, 128 | pos: usize = 0, 129 | old_pos: usize = 0, 130 | cols: usize, 131 | max_rows: usize = 0, 132 | 133 | const Self = @This(); 134 | 135 | pub fn init(ln: *Linenoise, in: File, out: File, prompt: []const u8) Self { 136 | return .{ 137 | .allocator = ln.allocator, 138 | .ln = ln, 139 | 140 | .stdin = in, 141 | .stdout = out, 142 | .prompt = prompt, 143 | .cols = getColumns(in, out) catch 80, 144 | }; 145 | } 146 | 147 | pub fn browseCompletions(self: *Self) !?u8 { 148 | var input_buf: [1]u8 = undefined; 149 | var c: ?u8 = null; 150 | 151 | const fun = self.ln.completions_callback orelse return null; 152 | const completions = try fun(self.allocator, self.buf.items); 153 | defer { 154 | for (completions) |x| self.allocator.free(x); 155 | self.allocator.free(completions); 156 | } 157 | 158 | if (completions.len == 0) { 159 | try term.beep(); 160 | } else { 161 | var finished = false; 162 | var i: usize = 0; 163 | 164 | while (!finished) { 165 | if (i < completions.len) { 166 | // Change to completion nr. i 167 | // First, save buffer so we can restore it later 168 | const old_buf = try self.buf.toOwnedSlice(self.allocator); 169 | const old_pos = self.pos; 170 | 171 | // Show suggested completion 172 | self.buf = .{}; 173 | try self.buf.appendSlice(self.allocator, completions[i]); 174 | self.pos = self.buf.items.len; 175 | 176 | try self.refreshLine(); 177 | 178 | // Restore original buffer into state 179 | self.buf.deinit(self.allocator); 180 | var new_buf = ArrayList(u8).fromOwnedSlice(self.allocator, old_buf); 181 | self.buf = new_buf.moveToUnmanaged(); 182 | self.pos = old_pos; 183 | } else { 184 | // Return to original line 185 | try self.refreshLine(); 186 | } 187 | 188 | // Read next key 189 | const nread = try self.stdin.read(&input_buf); 190 | c = if (nread == 1) input_buf[0] else return error.NothingRead; 191 | 192 | switch (c.?) { 193 | key_tab => { 194 | // Next completion 195 | i = (i + 1) % (completions.len + 1); 196 | if (i == completions.len) try term.beep(); 197 | }, 198 | key_esc => { 199 | // Stop browsing completions, return to buffer displayed 200 | // prior to browsing completions 201 | if (i < completions.len) try self.refreshLine(); 202 | finished = true; 203 | }, 204 | else => { 205 | // Stop browsing completions, potentially use suggested 206 | // completion 207 | if (i < completions.len) { 208 | // Replace buffer with text in the selected 209 | // completion 210 | self.buf.deinit(self.allocator); 211 | self.buf = .{}; 212 | try self.buf.appendSlice(self.allocator, completions[i]); 213 | 214 | self.pos = self.buf.items.len; 215 | } 216 | finished = true; 217 | }, 218 | } 219 | } 220 | } 221 | 222 | return c; 223 | } 224 | 225 | fn getHint(self: *Self) !?[]const u8 { 226 | if (self.ln.hints_callback) |fun| { 227 | return try fun(self.allocator, self.buf.items); 228 | } 229 | 230 | return null; 231 | } 232 | 233 | fn refreshSingleLine(self: *Self) !void { 234 | var buf = bufferedWriter(self.stdout.writer()); 235 | var writer = buf.writer(); 236 | 237 | const hint = try self.getHint(); 238 | defer if (hint) |str| self.allocator.free(str); 239 | 240 | // Calculate widths 241 | const pos = width(self.buf.items[0..self.pos]); 242 | const prompt_width = width(self.prompt); 243 | const hint_width = if (hint) |str| width(str) else 0; 244 | const buf_width = width(self.buf.items); 245 | 246 | // Don't show hint/prompt when there is no space 247 | const show_prompt = prompt_width < self.cols; 248 | const display_prompt_width = if (show_prompt) prompt_width else 0; 249 | const show_hint = display_prompt_width + hint_width < self.cols; 250 | const display_hint_width = if (show_hint) hint_width else 0; 251 | 252 | // buffer -> display_buf mapping 253 | const avail_space = self.cols - display_prompt_width - display_hint_width - 1; 254 | const whole_buffer_fits = buf_width <= avail_space; 255 | var start: usize = undefined; 256 | var end: usize = undefined; 257 | if (whole_buffer_fits) { 258 | start = 0; 259 | end = self.buf.items.len; 260 | } else { 261 | if (pos < avail_space) { 262 | start = 0; 263 | end = try calculateStartOrEnd( 264 | self.allocator, 265 | .end, 266 | self.buf.items, 267 | avail_space, 268 | ); 269 | } else { 270 | end = self.pos; 271 | start = try calculateStartOrEnd( 272 | self.allocator, 273 | .start, 274 | self.buf.items[0..end], 275 | avail_space, 276 | ); 277 | } 278 | } 279 | const display_buf = self.buf.items[start..end]; 280 | 281 | // Move cursor to left edge 282 | try writer.writeAll("\r"); 283 | 284 | // Write prompt 285 | if (show_prompt) try writer.writeAll(self.prompt); 286 | 287 | // Write current buffer content 288 | if (self.ln.mask_mode) { 289 | for (display_buf) |_| { 290 | try writer.writeAll("*"); 291 | } 292 | } else { 293 | try writer.writeAll(display_buf); 294 | } 295 | 296 | // Show hints 297 | if (show_hint) { 298 | if (hint) |str| { 299 | try writer.writeAll(str); 300 | } 301 | } 302 | 303 | // Erase to the right 304 | try writer.writeAll("\x1b[0K"); 305 | 306 | // Move cursor to original position 307 | const cursor_pos = if (pos > avail_space) self.cols - display_hint_width - 1 else display_prompt_width + pos; 308 | try writer.print("\r\x1b[{}C", .{cursor_pos}); 309 | 310 | // Write buffer 311 | try buf.flush(); 312 | } 313 | 314 | fn refreshMultiLine(self: *Self) !void { 315 | var buf = bufferedWriter(self.stdout.writer()); 316 | var writer = buf.writer(); 317 | 318 | const hint = try self.getHint(); 319 | defer if (hint) |str| self.allocator.free(str); 320 | 321 | // Calculate widths 322 | const pos = width(self.buf.items[0..self.pos]); 323 | const prompt_width = width(self.prompt); 324 | const hint_width = if (hint) |str| width(str) else 0; 325 | const buf_width = width(self.buf.items); 326 | const total_width = prompt_width + buf_width + hint_width; 327 | 328 | var rows = (total_width + self.cols - 1) / self.cols; 329 | const old_rpos = (prompt_width + self.old_pos + self.cols) / self.cols; 330 | const old_max_rows = self.max_rows; 331 | 332 | if (rows > self.max_rows) { 333 | self.max_rows = rows; 334 | } 335 | 336 | // Go to the last row 337 | if (old_max_rows > old_rpos) { 338 | try writer.print("\x1B[{}B", .{old_max_rows - old_rpos}); 339 | } 340 | 341 | // Clear every row from bottom to top 342 | if (old_max_rows > 0) { 343 | var j: usize = 0; 344 | while (j < old_max_rows - 1) : (j += 1) { 345 | try writer.writeAll("\r\x1B[0K\x1B[1A"); 346 | } 347 | } 348 | 349 | // Clear the top line 350 | try writer.writeAll("\r\x1B[0K"); 351 | 352 | // Write prompt 353 | try writer.writeAll(self.prompt); 354 | 355 | // Write current buffer content 356 | if (self.ln.mask_mode) { 357 | for (self.buf.items) |_| { 358 | try writer.writeAll("*"); 359 | } 360 | } else { 361 | try writer.writeAll(self.buf.items); 362 | } 363 | 364 | // Show hints if applicable 365 | if (hint) |str| { 366 | try writer.writeAll(str); 367 | } 368 | 369 | // Reserve a newline if we filled all columns 370 | if (self.pos > 0 and self.pos == self.buf.items.len and total_width % self.cols == 0) { 371 | try writer.writeAll("\n\r"); 372 | rows += 1; 373 | if (rows > self.max_rows) { 374 | self.max_rows = rows; 375 | } 376 | } 377 | 378 | // Move cursor to right position: 379 | const rpos = (prompt_width + pos + self.cols) / self.cols; 380 | 381 | // First, y position (move up if necessary) 382 | if (rows > rpos) { 383 | try writer.print("\x1B[{}A", .{rows - rpos}); 384 | } 385 | 386 | // Then, x position (move right if necessary) 387 | const col = (prompt_width + pos) % self.cols; 388 | if (col > 0) { 389 | try writer.print("\r\x1B[{}C", .{col}); 390 | } else { 391 | try writer.writeAll("\r"); 392 | } 393 | 394 | self.old_pos = pos; 395 | 396 | try buf.flush(); 397 | } 398 | 399 | pub fn refreshLine(self: *Self) !void { 400 | if (self.ln.multiline_mode) { 401 | try self.refreshMultiLine(); 402 | } else { 403 | try self.refreshSingleLine(); 404 | } 405 | } 406 | 407 | pub fn editInsert(self: *Self, c: []const u8) !void { 408 | try self.buf.resize(self.allocator, self.buf.items.len + c.len); 409 | if (self.buf.items.len > 0 and self.pos < self.buf.items.len - c.len) { 410 | std.mem.copyBackwards( 411 | u8, 412 | self.buf.items[self.pos + c.len .. self.buf.items.len], 413 | self.buf.items[self.pos .. self.buf.items.len - c.len], 414 | ); 415 | } 416 | 417 | @memcpy(self.buf.items[self.pos..][0..c.len], c); 418 | self.pos += c.len; 419 | try self.refreshLine(); 420 | } 421 | 422 | fn prevCodepointLen(self: *Self, pos: usize) usize { 423 | if (pos >= 1 and @clz(~self.buf.items[pos - 1]) == 0) { 424 | return 1; 425 | } else if (pos >= 2 and @clz(~self.buf.items[pos - 2]) == 2) { 426 | return 2; 427 | } else if (pos >= 3 and @clz(~self.buf.items[pos - 3]) == 3) { 428 | return 3; 429 | } else if (pos >= 4 and @clz(~self.buf.items[pos - 4]) == 4) { 430 | return 4; 431 | } else { 432 | return 0; 433 | } 434 | } 435 | 436 | pub fn editMoveLeft(self: *Self) !void { 437 | if (self.pos == 0) return; 438 | self.pos -= self.prevCodepointLen(self.pos); 439 | try self.refreshLine(); 440 | } 441 | 442 | pub fn editMoveRight(self: *Self) !void { 443 | if (self.pos < self.buf.items.len) { 444 | const utf8_len = std.unicode.utf8ByteSequenceLength(self.buf.items[self.pos]) catch 1; 445 | self.pos += utf8_len; 446 | try self.refreshLine(); 447 | } 448 | } 449 | 450 | pub fn editMoveWordEnd(self: *Self) !void { 451 | if (self.pos < self.buf.items.len) { 452 | while (self.pos < self.buf.items.len and self.buf.items[self.pos] == ' ') 453 | self.pos += 1; 454 | while (self.pos < self.buf.items.len and self.buf.items[self.pos] != ' ') 455 | self.pos += 1; 456 | try self.refreshLine(); 457 | } 458 | } 459 | 460 | pub fn editMoveWordStart(self: *Self) !void { 461 | if (self.buf.items.len > 0 and self.pos > 0) { 462 | while (self.pos > 0 and self.buf.items[self.pos - 1] == ' ') 463 | self.pos -= 1; 464 | while (self.pos > 0 and self.buf.items[self.pos - 1] != ' ') 465 | self.pos -= 1; 466 | try self.refreshLine(); 467 | } 468 | } 469 | 470 | pub fn editMoveHome(self: *Self) !void { 471 | if (self.pos > 0) { 472 | self.pos = 0; 473 | try self.refreshLine(); 474 | } 475 | } 476 | 477 | pub fn editMoveEnd(self: *Self) !void { 478 | if (self.pos < self.buf.items.len) { 479 | self.pos = self.buf.items.len; 480 | try self.refreshLine(); 481 | } 482 | } 483 | 484 | pub const HistoryDirection = enum { 485 | next, 486 | prev, 487 | }; 488 | 489 | pub fn editHistoryNext(self: *Self, dir: HistoryDirection) !void { 490 | if (self.ln.history.hist.items.len > 0) { 491 | // Update the current history with the current line 492 | const old_index = self.ln.history.current; 493 | const current_entry = self.ln.history.hist.items[old_index]; 494 | self.ln.history.allocator.free(current_entry); 495 | self.ln.history.hist.items[old_index] = try self.ln.history.allocator.dupe(u8, self.buf.items); 496 | 497 | // Update history index 498 | const new_index = switch (dir) { 499 | .next => if (old_index < self.ln.history.hist.items.len - 1) old_index + 1 else self.ln.history.hist.items.len - 1, 500 | .prev => if (old_index > 0) old_index - 1 else 0, 501 | }; 502 | self.ln.history.current = new_index; 503 | 504 | // Copy history entry to the current line buffer 505 | self.buf.deinit(self.allocator); 506 | self.buf = .{}; 507 | try self.buf.appendSlice(self.allocator, self.ln.history.hist.items[new_index]); 508 | self.pos = self.buf.items.len; 509 | 510 | try self.refreshLine(); 511 | } 512 | } 513 | 514 | pub fn editDelete(self: *Self) !void { 515 | if (self.buf.items.len == 0 or self.pos >= self.buf.items.len) return; 516 | 517 | const utf8_len = std.unicode.utf8CodepointSequenceLength(self.buf.items[self.pos]) catch 1; 518 | std.mem.copyForwards( 519 | u8, 520 | self.buf.items[self.pos .. self.buf.items.len - utf8_len], 521 | self.buf.items[self.pos + utf8_len .. self.buf.items.len], 522 | ); 523 | try self.buf.resize(self.allocator, self.buf.items.len - utf8_len); 524 | try self.refreshLine(); 525 | } 526 | 527 | pub fn editBackspace(self: *Self) !void { 528 | if (self.buf.items.len == 0 or self.pos == 0) return; 529 | 530 | const utf8_len = self.prevCodepointLen(self.pos); 531 | std.mem.copyForwards( 532 | u8, 533 | self.buf.items[self.pos - utf8_len .. self.buf.items.len - utf8_len], 534 | self.buf.items[self.pos..self.buf.items.len], 535 | ); 536 | self.pos -= utf8_len; 537 | try self.buf.resize(self.allocator, self.buf.items.len - utf8_len); 538 | try self.refreshLine(); 539 | } 540 | 541 | pub fn editSwapPrev(self: *Self) !void { 542 | // aaa bb => bb aaa 543 | // ^ ^ ^- pos_end ^ ^ ^- pos_end 544 | // | | | | 545 | // | pos_1 | pos_2 546 | // pos_begin pos_begin 547 | 548 | const pos_end = self.pos; 549 | const b_len = self.prevCodepointLen(pos_end); 550 | const pos_1 = pos_end - b_len; 551 | const a_len = self.prevCodepointLen(pos_1); 552 | const pos_begin = pos_1 - a_len; 553 | const pos_2 = pos_begin + b_len; 554 | 555 | if (a_len == 0 or b_len == 0) return; 556 | 557 | // save both codepoints 558 | var tmp: [8]u8 = undefined; 559 | @memcpy( 560 | tmp[0 .. a_len + b_len], 561 | self.buf.items[pos_begin..pos_end], 562 | ); 563 | // write b from tmp 564 | @memcpy( 565 | self.buf.items[pos_begin..pos_2], 566 | tmp[a_len .. a_len + b_len], 567 | ); 568 | // write a from tmp 569 | @memcpy( 570 | self.buf.items[pos_2..pos_end], 571 | tmp[0..a_len], 572 | ); 573 | 574 | try self.refreshLine(); 575 | } 576 | 577 | pub fn editDeletePrevWord(self: *Self) !void { 578 | if (self.buf.items.len == 0 or self.pos == 0) return; 579 | 580 | const old_pos = self.pos; 581 | while (self.pos > 0 and self.buf.items[self.pos - 1] == ' ') 582 | self.pos -= 1; 583 | while (self.pos > 0 and self.buf.items[self.pos - 1] != ' ') 584 | self.pos -= 1; 585 | 586 | const diff = old_pos - self.pos; 587 | const new_len = self.buf.items.len - diff; 588 | std.mem.copyForwards( 589 | u8, 590 | self.buf.items[self.pos..new_len], 591 | self.buf.items[old_pos..self.buf.items.len], 592 | ); 593 | try self.buf.resize(self.allocator, new_len); 594 | try self.refreshLine(); 595 | } 596 | 597 | pub fn editKillLineForward(self: *Self) !void { 598 | try self.buf.resize(self.allocator, self.pos); 599 | try self.refreshLine(); 600 | } 601 | 602 | pub fn editKillLineBackward(self: *Self) !void { 603 | const new_len = self.buf.items.len - self.pos; 604 | std.mem.copyForwards( 605 | u8, 606 | self.buf.items[0..new_len], 607 | self.buf.items[self.pos..self.buf.items.len], 608 | ); 609 | self.pos = 0; 610 | try self.buf.resize(self.allocator, new_len); 611 | try self.refreshLine(); 612 | } 613 | }; 614 | -------------------------------------------------------------------------------- /src/term.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const File = std.fs.File; 4 | 5 | const unsupported_term = [_][]const u8{ "dumb", "cons25", "emacs" }; 6 | 7 | const is_windows = builtin.os.tag == .windows; 8 | const termios = if (!is_windows) std.posix.termios else struct { inMode: w.DWORD, outMode: w.DWORD }; 9 | 10 | pub fn isUnsupportedTerm(allocator: std.mem.Allocator) bool { 11 | const env_var = std.process.getEnvVarOwned(allocator, "TERM") catch return false; 12 | defer allocator.free(env_var); 13 | return for (unsupported_term) |t| { 14 | if (std.ascii.eqlIgnoreCase(env_var, t)) 15 | break true; 16 | } else false; 17 | } 18 | 19 | const w = struct { 20 | pub usingnamespace std.os.windows; 21 | pub const ENABLE_VIRTUAL_TERMINAL_INPUT = @as(c_int, 0x200); 22 | pub const CP_UTF8 = @as(c_int, 65001); 23 | pub const INPUT_RECORD = extern struct { 24 | EventType: w.WORD, 25 | _ignored: [16]u8, 26 | }; 27 | }; 28 | 29 | const k32 = struct { 30 | pub usingnamespace std.os.windows.kernel32; 31 | pub extern "kernel32" fn SetConsoleCP(wCodePageID: w.UINT) callconv(w.WINAPI) w.BOOL; 32 | pub extern "kernel32" fn PeekConsoleInputW(hConsoleInput: w.HANDLE, lpBuffer: [*]w.INPUT_RECORD, nLength: w.DWORD, lpNumberOfEventsRead: ?*w.DWORD) callconv(w.WINAPI) w.BOOL; 33 | pub extern "kernel32" fn ReadConsoleW(hConsoleInput: w.HANDLE, lpBuffer: [*]u16, nNumberOfCharsToRead: w.DWORD, lpNumberOfCharsRead: ?*w.DWORD, lpReserved: ?*anyopaque) callconv(w.WINAPI) w.BOOL; 34 | }; 35 | 36 | pub fn enableRawMode(in: File, out: File) !termios { 37 | if (is_windows) { 38 | var result: termios = .{ 39 | .inMode = 0, 40 | .outMode = 0, 41 | }; 42 | var irec: [1]w.INPUT_RECORD = undefined; 43 | var n: w.DWORD = 0; 44 | if (k32.PeekConsoleInputW(in.handle, &irec, 1, &n) == 0 or 45 | k32.GetConsoleMode(in.handle, &result.inMode) == 0 or 46 | k32.GetConsoleMode(out.handle, &result.outMode) == 0) 47 | return error.InitFailed; 48 | _ = k32.SetConsoleMode(in.handle, w.ENABLE_VIRTUAL_TERMINAL_INPUT); 49 | _ = k32.SetConsoleMode(out.handle, result.outMode | w.ENABLE_VIRTUAL_TERMINAL_PROCESSING); 50 | _ = k32.SetConsoleCP(w.CP_UTF8); 51 | _ = k32.SetConsoleOutputCP(w.CP_UTF8); 52 | return result; 53 | } else { 54 | const orig = try std.posix.tcgetattr(in.handle); 55 | var raw = orig; 56 | 57 | raw.iflag.BRKINT = false; 58 | raw.iflag.ICRNL = false; 59 | raw.iflag.INPCK = false; 60 | raw.iflag.ISTRIP = false; 61 | raw.iflag.IXON = false; 62 | 63 | raw.oflag.OPOST = false; 64 | 65 | raw.cflag.CSIZE = .CS8; 66 | 67 | raw.lflag.ECHO = false; 68 | raw.lflag.ICANON = false; 69 | raw.lflag.IEXTEN = false; 70 | raw.lflag.ISIG = false; 71 | 72 | // FIXME 73 | // raw.cc[std.os.VMIN] = 1; 74 | // raw.cc[std.os.VTIME] = 0; 75 | 76 | try std.posix.tcsetattr(in.handle, std.posix.TCSA.FLUSH, raw); 77 | 78 | return orig; 79 | } 80 | } 81 | 82 | pub fn disableRawMode(in: File, out: File, orig: termios) void { 83 | if (is_windows) { 84 | _ = k32.SetConsoleMode(in.handle, orig.inMode); 85 | _ = k32.SetConsoleMode(out.handle, orig.outMode); 86 | } else { 87 | std.posix.tcsetattr(in.handle, std.posix.TCSA.FLUSH, orig) catch {}; 88 | } 89 | } 90 | 91 | fn getCursorPosition(in: File, out: File) !usize { 92 | var buf: [32]u8 = undefined; 93 | var reader = in.reader(); 94 | 95 | // Tell terminal to report cursor to in 96 | try out.writeAll("\x1B[6n"); 97 | 98 | // Read answer 99 | const answer = (try reader.readUntilDelimiterOrEof(&buf, 'R')) orelse 100 | return error.CursorPos; 101 | 102 | // Parse answer 103 | if (!std.mem.startsWith(u8, "\x1B[", answer)) 104 | return error.CursorPos; 105 | 106 | var iter = std.mem.splitScalar(u8, answer[2..], ';'); 107 | _ = iter.next() orelse return error.CursorPos; 108 | const x = iter.next() orelse return error.CursorPos; 109 | 110 | return try std.fmt.parseInt(usize, x, 10); 111 | } 112 | 113 | fn getColumnsFallback(in: File, out: File) !usize { 114 | var writer = out.writer(); 115 | const orig_cursor_pos = try getCursorPosition(in, out); 116 | 117 | try writer.print("\x1B[999C", .{}); 118 | const cols = try getCursorPosition(in, out); 119 | 120 | try writer.print("\x1B[{}D", .{orig_cursor_pos}); 121 | 122 | return cols; 123 | } 124 | 125 | pub fn getColumns(in: File, out: File) !usize { 126 | switch (builtin.os.tag) { 127 | .windows => { 128 | var csbi: w.CONSOLE_SCREEN_BUFFER_INFO = undefined; 129 | _ = k32.GetConsoleScreenBufferInfo(out.handle, &csbi); 130 | return @intCast(csbi.dwSize.X); 131 | }, 132 | else => { 133 | var winsize: std.posix.winsize = .{ 134 | .row = 0, 135 | .col = 0, 136 | .xpixel = 0, 137 | .ypixel = 0, 138 | }; 139 | 140 | const err = std.posix.system.ioctl(in.handle, std.posix.T.IOCGWINSZ, @intFromPtr(&winsize)); 141 | if (std.posix.errno(err) == .SUCCESS and winsize.col > 0) { 142 | return winsize.col; 143 | } else { 144 | return try getColumnsFallback(in, out); 145 | } 146 | }, 147 | } 148 | } 149 | 150 | pub fn clearScreen() !void { 151 | const stdout = std.io.getStdErr(); 152 | try stdout.writeAll("\x1b[H\x1b[2J"); 153 | } 154 | 155 | pub fn beep() !void { 156 | const stderr = std.io.getStdErr(); 157 | try stderr.writeAll("\x07"); 158 | } 159 | 160 | var utf8ConsoleBuffer = [_]u8{0} ** 10; 161 | var utf8ConsoleReadBytes: usize = 0; 162 | 163 | // this is needed due to a bug in win32 console: https://github.com/microsoft/terminal/issues/4551 164 | fn readWin32Console(self: File, buffer: []u8) !usize { 165 | var toRead = buffer.len; 166 | while (toRead > 0) { 167 | if (utf8ConsoleReadBytes > 0) { 168 | const existing = @min(toRead, utf8ConsoleReadBytes); 169 | @memcpy(buffer[(buffer.len - toRead)..], utf8ConsoleBuffer[0..existing]); 170 | utf8ConsoleReadBytes -= existing; 171 | if (utf8ConsoleReadBytes > 0) 172 | std.mem.copyForwards(u8, &utf8ConsoleBuffer, utf8ConsoleBuffer[existing..]); 173 | toRead -= existing; 174 | continue; 175 | } 176 | var charsRead: w.DWORD = 0; 177 | var wideBuf: [2]w.WCHAR = undefined; 178 | if (k32.ReadConsoleW(self.handle, &wideBuf, 1, &charsRead, null) == 0) 179 | return 0; 180 | if (charsRead == 0) 181 | break; 182 | const wideBufLen: u8 = if (wideBuf[0] >= 0xD800 and wideBuf[0] <= 0xDBFF) _: { 183 | // read surrogate 184 | if (k32.ReadConsoleW(self.handle, wideBuf[1..], 1, &charsRead, null) == 0) 185 | return 0; 186 | if (charsRead == 0) 187 | break; 188 | break :_ 2; 189 | } else 1; 190 | //WideCharToMultiByte(GetConsoleCP(), 0, buf, bufLen, converted, sizeof(converted), NULL, NULL); 191 | utf8ConsoleReadBytes += try std.unicode.utf16LeToUtf8(&utf8ConsoleBuffer, wideBuf[0..wideBufLen]); 192 | } 193 | return buffer.len - toRead; 194 | } 195 | 196 | pub const read = if (is_windows) readWin32Console else File.read; 197 | -------------------------------------------------------------------------------- /src/unicode.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Allocator = std.mem.Allocator; 3 | const expectEqualSlices = std.testing.expectEqualSlices; 4 | 5 | const wcwidth = @import("wcwidth").wcwidth; 6 | 7 | pub fn width(s: []const u8) usize { 8 | var result: usize = 0; 9 | 10 | var escape_seq = false; 11 | const view = std.unicode.Utf8View.init(s) catch return 0; 12 | var iter = view.iterator(); 13 | while (iter.nextCodepoint()) |codepoint| { 14 | if (escape_seq) { 15 | if (codepoint == 'm') { 16 | escape_seq = false; 17 | } 18 | } else { 19 | if (codepoint == '\x1b') { 20 | escape_seq = true; 21 | } else { 22 | const wcw = wcwidth(codepoint); 23 | if (wcw < 0) return 0; 24 | result += @intCast(wcw); 25 | } 26 | } 27 | } 28 | 29 | return result; 30 | } 31 | --------------------------------------------------------------------------------