Compare commits
1 commit
main
...
panic-mv-r
Author | SHA1 | Date | |
---|---|---|---|
![]() |
678f84b3a9 |
12 changed files with 73 additions and 389 deletions
172
README.md
172
README.md
|
@ -9,6 +9,8 @@ It begins with them, but ends with me. Their son, Vaxis
|
|||
Libvaxis _does not use terminfo_. Support for vt features is detected through
|
||||
terminal queries.
|
||||
|
||||
Contributions are welcome.
|
||||
|
||||
Vaxis uses zig `0.13.0`.
|
||||
|
||||
## Features
|
||||
|
@ -35,173 +37,7 @@ Unix-likes.
|
|||
|
||||
[Documentation](https://rockorager.github.io/libvaxis/#vaxis.Vaxis)
|
||||
|
||||
The library provides both a low level API suitable for making applications of
|
||||
any sort as well as a higher level framework. The low level API is suitable for
|
||||
making applications of any type, providing your own event loop, and gives you
|
||||
full control over each cell on the screen.
|
||||
|
||||
The high level API, called `vxfw` (Vaxis framework), provides a Flutter-like
|
||||
style of API. The framework provides an application runtime which handles the
|
||||
event loop, focus management, mouse handling, and more. Several widgets are
|
||||
provided, and custom widgets are easy to build. This API is most likely what you
|
||||
want to use for typical TUI applications.
|
||||
|
||||
### vxfw (Vaxis framework)
|
||||
|
||||
Let's build a simple button counter application. This example can be run using
|
||||
the command `zig build example -Dexample=counter`. The below application has
|
||||
full mouse support: the button *and mouse shape* will change style on hover, on
|
||||
click, and has enough logic to cancel a press if the release does not occur over
|
||||
the button. Try it! Click the button, move the mouse off the button and release.
|
||||
All of this logic is baked into the base `Button` widget.
|
||||
|
||||
```zig
|
||||
const std = @import("std");
|
||||
const vaxis = @import("vaxis");
|
||||
const vxfw = vaxis.vxfw;
|
||||
|
||||
/// Our main application state
|
||||
const Model = struct {
|
||||
/// State of the counter
|
||||
count: u32 = 0,
|
||||
/// The button. This widget is stateful and must live between frames
|
||||
button: vxfw.Button,
|
||||
|
||||
/// Helper function to return a vxfw.Widget struct
|
||||
pub fn widget(self: *Model) vxfw.Widget {
|
||||
return .{
|
||||
.userdata = self,
|
||||
.eventHandler = Model.typeErasedEventHandler,
|
||||
.drawFn = Model.typeErasedDrawFn,
|
||||
};
|
||||
}
|
||||
|
||||
/// This function will be called from the vxfw runtime.
|
||||
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
|
||||
const self: *Model = @ptrCast(@alignCast(ptr));
|
||||
switch (event) {
|
||||
// The root widget is always sent an init event as the first event. Users of the
|
||||
// library can also send this event to other widgets they create if they need to do
|
||||
// some initialization.
|
||||
.init => return ctx.requestFocus(self.button.widget()),
|
||||
.key_press => |key| {
|
||||
if (key.matches('c', .{ .ctrl = true })) {
|
||||
ctx.quit = true;
|
||||
return;
|
||||
}
|
||||
},
|
||||
// We can request a specific widget gets focus. In this case, we always want to focus
|
||||
// our button. Having focus means that key events will be sent up the widget tree to
|
||||
// the focused widget, and then bubble back down the tree to the root. Users can tell
|
||||
// the runtime the event was handled and the capture or bubble phase will stop
|
||||
.focus_in => return ctx.requestFocus(self.button.widget()),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is called from the vxfw runtime. It will be called on a regular interval, and
|
||||
/// only when any event handler has marked the redraw flag in EventContext as true. By
|
||||
/// explicitly requiring setting the redraw flag, vxfw can prevent excessive redraws for events
|
||||
/// which don't change state (ie mouse motion, unhandled key events, etc)
|
||||
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
|
||||
const self: *Model = @ptrCast(@alignCast(ptr));
|
||||
// The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum
|
||||
// constraint. The minimum constraint will always be set, even if it is set to 0x0. The
|
||||
// maximum constraint can have null width and/or height - meaning there is no constraint in
|
||||
// that direction and the widget should take up as much space as it needs. By calling size()
|
||||
// on the max, we assert that it has some constrained size. This is *always* the case for
|
||||
// the root widget - the maximum size will always be the size of the terminal screen.
|
||||
const max_size = ctx.max.size();
|
||||
|
||||
// The DrawContext also contains an arena allocator that can be used for each frame. The
|
||||
// lifetime of this allocation is until the next time we draw a frame. This is useful for
|
||||
// temporary allocations such as the one below: we have an integer we want to print as text.
|
||||
// We can safely allocate this with the ctx arena since we only need it for this frame.
|
||||
const count_text = try std.fmt.allocPrint(ctx.arena, "{d}", .{self.count});
|
||||
const text: vxfw.Text = .{ .text = count_text };
|
||||
|
||||
// Each widget returns a Surface from it's draw function. A Surface contains the rectangular
|
||||
// area of the widget, as well as some information about the surface or widget: can we focus
|
||||
// it? does it handle the mouse?
|
||||
//
|
||||
// It DOES NOT contain the location it should be within it's parent. Only the parent can set
|
||||
// this via a SubSurface. Here, we will return a Surface for the root widget (Model), which
|
||||
// has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface
|
||||
// with an offset and a z-index - the offset can be negative. This lets a parent draw a
|
||||
// child and place it within itself
|
||||
const text_child: vxfw.SubSurface = .{
|
||||
.origin = .{ .row = 0, .col = 0 },
|
||||
.surface = try text.draw(ctx),
|
||||
};
|
||||
|
||||
const button_child: vxfw.SubSurface = .{
|
||||
.origin = .{ .row = 2, .col = 0 },
|
||||
.surface = try self.button.draw(ctx.withConstraints(
|
||||
ctx.min,
|
||||
// Here we explicitly set a new maximum size constraint for the Button. A Button will
|
||||
// expand to fill it's area and must have some hard limit in the maximum constraint
|
||||
.{ .width = 16, .height = 3 },
|
||||
)),
|
||||
};
|
||||
|
||||
// We also can use our arena to allocate the slice for our SubSurfaces. This slice only
|
||||
// needs to live until the next frame, making this safe.
|
||||
const children = try ctx.arena.alloc(vxfw.SubSurface, 2);
|
||||
children[0] = text_child;
|
||||
children[1] = button_child;
|
||||
|
||||
return .{
|
||||
// A Surface must have a size. Our root widget is the size of the screen
|
||||
.size = max_size,
|
||||
.widget = self.widget(),
|
||||
.focusable = false,
|
||||
// We didn't actually need to draw anything for the root. In this case, we can set
|
||||
// buffer to a zero length slice. If this slice is *not zero length*, the runtime will
|
||||
// assert that it's length is equal to the size.width * size.height.
|
||||
.buffer = &.{},
|
||||
.children = children,
|
||||
};
|
||||
}
|
||||
|
||||
/// The onClick callback for our button. This is also called if we press enter while the button
|
||||
/// has focus
|
||||
fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
|
||||
const ptr = maybe_ptr orelse return;
|
||||
const self: *Model = @ptrCast(@alignCast(ptr));
|
||||
self.count +|= 1;
|
||||
return ctx.consumeAndRedraw();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app = try vxfw.App.init(allocator);
|
||||
defer app.deinit();
|
||||
|
||||
// We heap allocate our model because we will require a stable pointer to it in our Button
|
||||
// widget
|
||||
const model = try allocator.create(Model);
|
||||
defer allocator.destroy(model);
|
||||
|
||||
// Set the initial state of our button
|
||||
model.* = .{
|
||||
.count = 0,
|
||||
.button = .{
|
||||
.label = "Click me!",
|
||||
.onClick = Model.onClick,
|
||||
.userdata = model,
|
||||
},
|
||||
};
|
||||
|
||||
try app.run(model.widget(), .{});
|
||||
}
|
||||
```
|
||||
|
||||
### Low level API
|
||||
[Starter repo](https://github.com/rockorager/libvaxis-starter)
|
||||
|
||||
Vaxis requires three basic primitives to operate:
|
||||
|
||||
|
@ -220,6 +56,8 @@ also handle these query responses and update the Vaxis.caps struct accordingly.
|
|||
See the `Loop` implementation to see how this is done if writing your own event
|
||||
loop.
|
||||
|
||||
## Example
|
||||
|
||||
```zig
|
||||
const std = @import("std");
|
||||
const vaxis = @import("vaxis");
|
||||
|
|
|
@ -29,7 +29,6 @@ pub fn build(b: *std.Build) void {
|
|||
// Examples
|
||||
const Example = enum {
|
||||
cli,
|
||||
counter,
|
||||
fuzzy,
|
||||
image,
|
||||
main,
|
||||
|
|
|
@ -62,7 +62,7 @@ pub fn main() !void {
|
|||
} else {
|
||||
selected_option.? = selected_option.? -| 1;
|
||||
}
|
||||
} else if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) {
|
||||
} else if (key.matches(vaxis.Key.enter, .{})) {
|
||||
if (selected_option) |i| {
|
||||
log.err("enter", .{});
|
||||
try text_input.insertSliceAtCursor(options[i]);
|
||||
|
|
|
@ -1,140 +0,0 @@
|
|||
const std = @import("std");
|
||||
const vaxis = @import("vaxis");
|
||||
const vxfw = vaxis.vxfw;
|
||||
|
||||
/// Our main application state
|
||||
const Model = struct {
|
||||
/// State of the counter
|
||||
count: u32 = 0,
|
||||
/// The button. This widget is stateful and must live between frames
|
||||
button: vxfw.Button,
|
||||
|
||||
/// Helper function to return a vxfw.Widget struct
|
||||
pub fn widget(self: *Model) vxfw.Widget {
|
||||
return .{
|
||||
.userdata = self,
|
||||
.eventHandler = Model.typeErasedEventHandler,
|
||||
.drawFn = Model.typeErasedDrawFn,
|
||||
};
|
||||
}
|
||||
|
||||
/// This function will be called from the vxfw runtime.
|
||||
fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
|
||||
const self: *Model = @ptrCast(@alignCast(ptr));
|
||||
switch (event) {
|
||||
// The root widget is always sent an init event as the first event. Users of the
|
||||
// library can also send this event to other widgets they create if they need to do
|
||||
// some initialization.
|
||||
.init => return ctx.requestFocus(self.button.widget()),
|
||||
.key_press => |key| {
|
||||
if (key.matches('c', .{ .ctrl = true })) {
|
||||
ctx.quit = true;
|
||||
return;
|
||||
}
|
||||
},
|
||||
// We can request a specific widget gets focus. In this case, we always want to focus
|
||||
// our button. Having focus means that key events will be sent up the widget tree to
|
||||
// the focused widget, and then bubble back down the tree to the root. Users can tell
|
||||
// the runtime the event was handled and the capture or bubble phase will stop
|
||||
.focus_in => return ctx.requestFocus(self.button.widget()),
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// This function is called from the vxfw runtime. It will be called on a regular interval, and
|
||||
/// only when any event handler has marked the redraw flag in EventContext as true. By
|
||||
/// explicitly requiring setting the redraw flag, vxfw can prevent excessive redraws for events
|
||||
/// which don't change state (ie mouse motion, unhandled key events, etc)
|
||||
fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) std.mem.Allocator.Error!vxfw.Surface {
|
||||
const self: *Model = @ptrCast(@alignCast(ptr));
|
||||
// The DrawContext is inspired from Flutter. Each widget will receive a minimum and maximum
|
||||
// constraint. The minimum constraint will always be set, even if it is set to 0x0. The
|
||||
// maximum constraint can have null width and/or height - meaning there is no constraint in
|
||||
// that direction and the widget should take up as much space as it needs. By calling size()
|
||||
// on the max, we assert that it has some constrained size. This is *always* the case for
|
||||
// the root widget - the maximum size will always be the size of the terminal screen.
|
||||
const max_size = ctx.max.size();
|
||||
|
||||
// The DrawContext also contains an arena allocator that can be used for each frame. The
|
||||
// lifetime of this allocation is until the next time we draw a frame. This is useful for
|
||||
// temporary allocations such as the one below: we have an integer we want to print as text.
|
||||
// We can safely allocate this with the ctx arena since we only need it for this frame.
|
||||
if (self.count > 0) {
|
||||
self.button.label = try std.fmt.allocPrint(ctx.arena, "Clicks: {d}", .{self.count});
|
||||
} else {
|
||||
self.button.label = "Click me!";
|
||||
}
|
||||
|
||||
// Each widget returns a Surface from it's draw function. A Surface contains the rectangular
|
||||
// area of the widget, as well as some information about the surface or widget: can we focus
|
||||
// it? does it handle the mouse?
|
||||
//
|
||||
// It DOES NOT contain the location it should be within it's parent. Only the parent can set
|
||||
// this via a SubSurface. Here, we will return a Surface for the root widget (Model), which
|
||||
// has two SubSurfaces: one for the text and one for the button. A SubSurface is a Surface
|
||||
// with an offset and a z-index - the offset can be negative. This lets a parent draw a
|
||||
// child and place it within itself
|
||||
const button_child: vxfw.SubSurface = .{
|
||||
.origin = .{ .row = 0, .col = 0 },
|
||||
.surface = try self.button.draw(ctx.withConstraints(
|
||||
ctx.min,
|
||||
// Here we explicitly set a new maximum size constraint for the Button. A Button will
|
||||
// expand to fill it's area and must have some hard limit in the maximum constraint
|
||||
.{ .width = 16, .height = 3 },
|
||||
)),
|
||||
};
|
||||
|
||||
// We also can use our arena to allocate the slice for our SubSurfaces. This slice only
|
||||
// needs to live until the next frame, making this safe.
|
||||
const children = try ctx.arena.alloc(vxfw.SubSurface, 1);
|
||||
children[0] = button_child;
|
||||
|
||||
return .{
|
||||
// A Surface must have a size. Our root widget is the size of the screen
|
||||
.size = max_size,
|
||||
.widget = self.widget(),
|
||||
.focusable = false,
|
||||
// We didn't actually need to draw anything for the root. In this case, we can set
|
||||
// buffer to a zero length slice. If this slice is *not zero length*, the runtime will
|
||||
// assert that it's length is equal to the size.width * size.height.
|
||||
.buffer = &.{},
|
||||
.children = children,
|
||||
};
|
||||
}
|
||||
|
||||
/// The onClick callback for our button. This is also called if we press enter while the button
|
||||
/// has focus
|
||||
fn onClick(maybe_ptr: ?*anyopaque, ctx: *vxfw.EventContext) anyerror!void {
|
||||
const ptr = maybe_ptr orelse return;
|
||||
const self: *Model = @ptrCast(@alignCast(ptr));
|
||||
self.count +|= 1;
|
||||
return ctx.consumeAndRedraw();
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
|
||||
defer _ = gpa.deinit();
|
||||
|
||||
const allocator = gpa.allocator();
|
||||
|
||||
var app = try vxfw.App.init(allocator);
|
||||
defer app.deinit();
|
||||
|
||||
// We heap allocate our model because we will require a stable pointer to it in our Button
|
||||
// widget
|
||||
const model = try allocator.create(Model);
|
||||
defer allocator.destroy(model);
|
||||
|
||||
// Set the initial state of our button
|
||||
model.* = .{
|
||||
.count = 0,
|
||||
.button = .{
|
||||
.label = "Click me!",
|
||||
.onClick = Model.onClick,
|
||||
.userdata = model,
|
||||
},
|
||||
};
|
||||
|
||||
try app.run(model.widget(), .{});
|
||||
}
|
|
@ -167,12 +167,12 @@ pub fn main() !void {
|
|||
demo_tbl.sel_rows = try rows_list.toOwnedSlice();
|
||||
}
|
||||
// See Row Content
|
||||
if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) see_content = !see_content;
|
||||
if (key.matches(vaxis.Key.enter, .{})) see_content = !see_content;
|
||||
},
|
||||
.btm => {
|
||||
if (key.matchesAny(&.{ vaxis.Key.up, 'k' }, .{}) and moving) active = .mid
|
||||
// Run Command and Clear Command Bar
|
||||
else if (key.matchExact(vaxis.Key.enter, .{}) or key.matchExact('j', .{ .ctrl = true })) {
|
||||
else if (key.matchExact(vaxis.Key.enter, .{})) {
|
||||
const cmd = try cmd_input.toOwnedSlice();
|
||||
defer alloc.free(cmd);
|
||||
if (mem.eql(u8, ":q", cmd) or
|
||||
|
|
|
@ -99,7 +99,7 @@ pub fn main() !void {
|
|||
try loop.start();
|
||||
try vx.enterAltScreen(tty.anyWriter());
|
||||
vx.queueRefresh();
|
||||
} else if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) {
|
||||
} else if (key.matches(vaxis.Key.enter, .{})) {
|
||||
text_input.clearAndFree();
|
||||
} else {
|
||||
try text_input.update(.{ .key_press = key });
|
||||
|
|
|
@ -300,13 +300,14 @@ pub fn handleEventGeneric(self: anytype, vx: *Vaxis, cache: *GraphemeCache, Even
|
|||
},
|
||||
.winsize => |winsize| {
|
||||
vx.state.in_band_resize = true;
|
||||
if (@hasField(Event, "winsize")) {
|
||||
return self.postEvent(.{ .winsize = winsize });
|
||||
}
|
||||
|
||||
switch (builtin.os.tag) {
|
||||
.windows => {},
|
||||
// Reset the signal handler if we are receiving in_band_resize
|
||||
else => Tty.resetSignalHandler(),
|
||||
}
|
||||
if (@hasField(Event, "winsize")) {
|
||||
return self.postEvent(.{ .winsize = winsize });
|
||||
else => self.tty.resetSignalHandler(),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
@ -94,8 +94,9 @@ inline fn parseGround(input: []const u8, data: *const grapheme.GraphemeData) !Re
|
|||
0x00 => .{ .codepoint = '@', .mods = .{ .ctrl = true } },
|
||||
0x08 => .{ .codepoint = Key.backspace },
|
||||
0x09 => .{ .codepoint = Key.tab },
|
||||
0x0A => .{ .codepoint = 'j', .mods = .{ .ctrl = true } },
|
||||
0x0D => .{ .codepoint = Key.enter },
|
||||
0x0A,
|
||||
0x0D,
|
||||
=> .{ .codepoint = Key.enter },
|
||||
0x01...0x07,
|
||||
0x0B...0x0C,
|
||||
0x0E...0x1A,
|
||||
|
|
121
src/tty.zig
121
src/tty.zig
|
@ -59,7 +59,9 @@ pub const PosixTty = struct {
|
|||
.handler = .{ .handler = PosixTty.handleWinch },
|
||||
.mask = switch (builtin.os.tag) {
|
||||
.macos => 0,
|
||||
else => posix.empty_sigset,
|
||||
.linux => posix.empty_sigset,
|
||||
.freebsd => posix.empty_sigset,
|
||||
else => @compileError("os not supported"),
|
||||
},
|
||||
.flags = 0,
|
||||
};
|
||||
|
@ -90,10 +92,12 @@ pub const PosixTty = struct {
|
|||
if (!handler_installed) return;
|
||||
handler_installed = false;
|
||||
var act = posix.Sigaction{
|
||||
.handler = .{ .handler = posix.SIG.DFL },
|
||||
.handler = posix.SIG.DFL,
|
||||
.mask = switch (builtin.os.tag) {
|
||||
.macos => 0,
|
||||
else => posix.empty_sigset,
|
||||
.linux => posix.empty_sigset,
|
||||
.freebsd => posix.empty_sigset,
|
||||
else => @compileError("os not supported"),
|
||||
},
|
||||
.flags = 0,
|
||||
};
|
||||
|
@ -214,8 +218,8 @@ pub const WindowsTty = struct {
|
|||
stdout: windows.HANDLE,
|
||||
|
||||
initial_codepage: c_uint,
|
||||
initial_input_mode: CONSOLE_MODE_INPUT,
|
||||
initial_output_mode: CONSOLE_MODE_OUTPUT,
|
||||
initial_input_mode: u32,
|
||||
initial_output_mode: u32,
|
||||
|
||||
// a buffer to write key text into
|
||||
buf: [4]u8 = undefined,
|
||||
|
@ -226,35 +230,58 @@ pub const WindowsTty = struct {
|
|||
|
||||
const utf8_codepage: c_uint = 65001;
|
||||
|
||||
/// The input mode set by init
|
||||
pub const input_raw_mode: CONSOLE_MODE_INPUT = .{
|
||||
.WINDOW_INPUT = 1, // resize events
|
||||
.MOUSE_INPUT = 1,
|
||||
.EXTENDED_FLAGS = 1, // allow mouse events
|
||||
const InputMode = struct {
|
||||
const enable_window_input: u32 = 0x0008; // resize events
|
||||
const enable_mouse_input: u32 = 0x0010;
|
||||
const enable_extended_flags: u32 = 0x0080; // allows mouse events
|
||||
|
||||
pub fn rawMode() u32 {
|
||||
return enable_window_input | enable_mouse_input | enable_extended_flags;
|
||||
}
|
||||
};
|
||||
|
||||
/// The output mode set by init
|
||||
pub const output_raw_mode: CONSOLE_MODE_OUTPUT = .{
|
||||
.PROCESSED_OUTPUT = 1, // handle control sequences
|
||||
.VIRTUAL_TERMINAL_PROCESSING = 1, // handle ANSI sequences
|
||||
.DISABLE_NEWLINE_AUTO_RETURN = 1, // disable inserting a new line when we write at the last column
|
||||
.ENABLE_LVB_GRID_WORLDWIDE = 1, // enables reverse video and underline
|
||||
const OutputMode = struct {
|
||||
const enable_processed_output: u32 = 0x0001; // handle control sequences
|
||||
const enable_virtual_terminal_processing: u32 = 0x0004; // handle ANSI sequences
|
||||
const disable_newline_auto_return: u32 = 0x0008; // disable inserting a new line when we write at the last column
|
||||
const enable_lvb_grid_worldwide: u32 = 0x0010; // enables reverse video and underline
|
||||
|
||||
fn rawMode() u32 {
|
||||
return enable_processed_output |
|
||||
enable_virtual_terminal_processing |
|
||||
disable_newline_auto_return |
|
||||
enable_lvb_grid_worldwide;
|
||||
}
|
||||
};
|
||||
|
||||
pub fn init() !Tty {
|
||||
const stdin = std.io.getStdIn().handle;
|
||||
const stdout = std.io.getStdOut().handle;
|
||||
const stdin = try windows.GetStdHandle(windows.STD_INPUT_HANDLE);
|
||||
const stdout = try windows.GetStdHandle(windows.STD_OUTPUT_HANDLE);
|
||||
|
||||
// get initial modes
|
||||
var initial_input_mode: windows.DWORD = undefined;
|
||||
var initial_output_mode: windows.DWORD = undefined;
|
||||
const initial_output_codepage = windows.kernel32.GetConsoleOutputCP();
|
||||
const initial_input_mode = try getConsoleMode(CONSOLE_MODE_INPUT, stdin);
|
||||
const initial_output_mode = try getConsoleMode(CONSOLE_MODE_OUTPUT, stdout);
|
||||
{
|
||||
if (windows.kernel32.GetConsoleMode(stdin, &initial_input_mode) == 0) {
|
||||
return windows.unexpectedError(windows.kernel32.GetLastError());
|
||||
}
|
||||
if (windows.kernel32.GetConsoleMode(stdout, &initial_output_mode) == 0) {
|
||||
return windows.unexpectedError(windows.kernel32.GetLastError());
|
||||
}
|
||||
}
|
||||
|
||||
// set new modes
|
||||
try setConsoleMode(stdin, input_raw_mode);
|
||||
try setConsoleMode(stdout, output_raw_mode);
|
||||
if (windows.kernel32.SetConsoleOutputCP(utf8_codepage) == 0)
|
||||
return windows.unexpectedError(windows.kernel32.GetLastError());
|
||||
{
|
||||
if (windows.kernel32.SetConsoleMode(stdin, InputMode.rawMode()) == 0)
|
||||
return windows.unexpectedError(windows.kernel32.GetLastError());
|
||||
|
||||
if (windows.kernel32.SetConsoleMode(stdout, OutputMode.rawMode()) == 0)
|
||||
return windows.unexpectedError(windows.kernel32.GetLastError());
|
||||
|
||||
if (windows.kernel32.SetConsoleOutputCP(utf8_codepage) == 0)
|
||||
return windows.unexpectedError(windows.kernel32.GetLastError());
|
||||
}
|
||||
|
||||
const self: Tty = .{
|
||||
.stdin = stdin,
|
||||
|
@ -272,50 +299,12 @@ pub const WindowsTty = struct {
|
|||
|
||||
pub fn deinit(self: Tty) void {
|
||||
_ = windows.kernel32.SetConsoleOutputCP(self.initial_codepage);
|
||||
setConsoleMode(self.stdin, self.initial_input_mode) catch {};
|
||||
setConsoleMode(self.stdout, self.initial_output_mode) catch {};
|
||||
_ = windows.kernel32.SetConsoleMode(self.stdin, self.initial_input_mode);
|
||||
_ = windows.kernel32.SetConsoleMode(self.stdout, self.initial_output_mode);
|
||||
windows.CloseHandle(self.stdin);
|
||||
windows.CloseHandle(self.stdout);
|
||||
}
|
||||
|
||||
pub const CONSOLE_MODE_INPUT = packed struct(u32) {
|
||||
PROCESSED_INPUT: u1 = 0,
|
||||
LINE_INPUT: u1 = 0,
|
||||
ECHO_INPUT: u1 = 0,
|
||||
WINDOW_INPUT: u1 = 0,
|
||||
MOUSE_INPUT: u1 = 0,
|
||||
INSERT_MODE: u1 = 0,
|
||||
QUICK_EDIT_MODE: u1 = 0,
|
||||
EXTENDED_FLAGS: u1 = 0,
|
||||
AUTO_POSITION: u1 = 0,
|
||||
VIRTUAL_TERMINAL_INPUT: u1 = 0,
|
||||
_: u22 = 0,
|
||||
};
|
||||
pub const CONSOLE_MODE_OUTPUT = packed struct(u32) {
|
||||
PROCESSED_OUTPUT: u1 = 0,
|
||||
WRAP_AT_EOL_OUTPUT: u1 = 0,
|
||||
VIRTUAL_TERMINAL_PROCESSING: u1 = 0,
|
||||
DISABLE_NEWLINE_AUTO_RETURN: u1 = 0,
|
||||
ENABLE_LVB_GRID_WORLDWIDE: u1 = 0,
|
||||
_: u27 = 0,
|
||||
};
|
||||
|
||||
pub fn getConsoleMode(comptime T: type, handle: windows.HANDLE) !T {
|
||||
var mode: u32 = undefined;
|
||||
if (windows.kernel32.GetConsoleMode(handle, &mode) == 0) return switch (windows.kernel32.GetLastError()) {
|
||||
.INVALID_HANDLE => error.InvalidHandle,
|
||||
else => |e| windows.unexpectedError(e),
|
||||
};
|
||||
return @bitCast(mode);
|
||||
}
|
||||
|
||||
pub fn setConsoleMode(handle: windows.HANDLE, mode: anytype) !void {
|
||||
if (windows.kernel32.SetConsoleMode(handle, @bitCast(mode)) == 0) return switch (windows.kernel32.GetLastError()) {
|
||||
.INVALID_HANDLE => error.InvalidHandle,
|
||||
else => |e| windows.unexpectedError(e),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn opaqueWrite(ptr: *const anyopaque, bytes: []const u8) !usize {
|
||||
const self: *const Tty = @ptrCast(@alignCast(ptr));
|
||||
return windows.WriteFile(self.stdout, bytes, null);
|
||||
|
@ -799,8 +788,4 @@ pub const TestTty = struct {
|
|||
pub fn nextEvent(_: *Tty, _: *Parser, _: ?std.mem.Allocator) !Event {
|
||||
return error.SkipZigTest;
|
||||
}
|
||||
|
||||
pub fn resetSignalHandler() void {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -44,7 +44,7 @@ fn typeErasedEventHandler(ptr: *anyopaque, ctx: *vxfw.EventContext, event: vxfw.
|
|||
pub fn handleEvent(self: *Button, ctx: *vxfw.EventContext, event: vxfw.Event) anyerror!void {
|
||||
switch (event) {
|
||||
.key_press => |key| {
|
||||
if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) {
|
||||
if (key.matches(vaxis.Key.enter, .{})) {
|
||||
return self.doClick(ctx);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@ const Allocator = std.mem.Allocator;
|
|||
|
||||
const Spinner = @This();
|
||||
|
||||
const frames: []const []const u8 = &.{ "⣶", "⣧", "⣏", "⡟", "⠿", "⢻", "⣹", "⣼" };
|
||||
const frames: []const []const u8 = &.{ "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" };
|
||||
const time_lapse: u32 = std.time.ms_per_s / 12; // 12 fps
|
||||
|
||||
count: std.atomic.Value(u16) = .{ .raw = 0 },
|
||||
|
|
|
@ -102,7 +102,7 @@ pub fn handleEvent(self: *TextField, ctx: *vxfw.EventContext, event: vxfw.Event)
|
|||
} else if (key.matches('d', .{ .alt = true })) {
|
||||
self.deleteWordAfter();
|
||||
return self.checkChanged(ctx);
|
||||
} else if (key.matches(vaxis.Key.enter, .{}) or key.matches('j', .{ .ctrl = true })) {
|
||||
} else if (key.matches(vaxis.Key.enter, .{})) {
|
||||
if (self.onSubmit) |onSubmit| {
|
||||
try onSubmit(self.userdata, ctx, self.previous_val);
|
||||
return ctx.consumeAndRedraw();
|
||||
|
|
Loading…
Add table
Reference in a new issue