+4
CHANGELOG.md
+4
CHANGELOG.md
···
1
1
# Changelog
2
2
3
+
## v1.1.0 (2025-05-21)
4
+
- fix(images): Improve performance by only locking critical parts of image loading
5
+
- fix(images): Thread the image loading process as not to block user input
6
+
3
7
## v1.0.1 (2025-04-14)
4
8
- fix(errors): Ensure logged enums are wrapped in `@tagName()` for readability.
5
9
-3
PROJECT_BOARD.md
-3
PROJECT_BOARD.md
+1
-1
build.zig
+1
-1
build.zig
···
2
2
const builtin = @import("builtin");
3
3
4
4
///Must match the `version` in `build.zig.zon`.
5
-
const version = std.SemanticVersion{ .major = 1, .minor = 0, .patch = 1 };
5
+
const version = std.SemanticVersion{ .major = 1, .minor = 1, .patch = 0 };
6
6
7
7
const targets: []const std.Target.Query = &.{
8
8
.{ .cpu_arch = .aarch64, .os_tag = .macos },
+1
-1
build.zig.zon
+1
-1
build.zig.zon
+27
-12
src/app.zig
+27
-12
src/app.zig
···
73
73
};
74
74
75
75
pub const Event = union(enum) {
76
+
image_ready,
76
77
key_press: Key,
77
78
winsize: vaxis.Winsize,
78
79
};
···
85
86
should_quit: bool,
86
87
vx: vaxis.Vaxis = undefined,
87
88
tty: vaxis.Tty = undefined,
89
+
loop: vaxis.Loop(Event) = undefined,
88
90
state: State = .normal,
89
91
actions: CircStack(Action, actions_len),
90
92
command_history: CommandHistory = CommandHistory{},
···
99
101
text_input_buf: [std.fs.max_path_bytes]u8 = undefined,
100
102
101
103
yanked: ?struct { dir: []const u8, entry: std.fs.Dir.Entry } = null,
102
-
image: ?vaxis.Image = null,
103
104
last_known_height: usize,
104
105
106
+
image: struct {
107
+
mutex: std.Thread.Mutex = .{},
108
+
data: ?vaxis.zigimg.Image = null,
109
+
path: ?[]const u8 = null,
110
+
} = .{},
111
+
105
112
pub fn init(alloc: std.mem.Allocator) !App {
106
113
var vx = try vaxis.init(alloc, .{
107
114
.kitty_keyboard_flags = .{
···
116
123
var help_menu = List([]const u8).init(alloc);
117
124
try help_menu.fromArray(&help_menu_items);
118
125
119
-
return App{
126
+
var app: App = .{
120
127
.alloc = alloc,
121
128
.should_quit = false,
122
129
.vx = vx,
···
127
134
.actions = CircStack(Action, actions_len).init(),
128
135
.last_known_height = vx.window().height,
129
136
};
137
+
138
+
app.loop = vaxis.Loop(Event){
139
+
.vaxis = &app.vx,
140
+
.tty = &app.tty,
141
+
};
142
+
143
+
return app;
130
144
}
131
145
132
146
pub fn deinit(self: *App) void {
···
157
171
self.vx.deinit(self.alloc, self.tty.anyWriter());
158
172
self.tty.deinit();
159
173
if (self.file_logger) |file_logger| file_logger.deinit();
174
+
if (self.image.path) |path| self.alloc.free(path);
175
+
if (self.image.data) |data| {
176
+
var img_data = data;
177
+
img_data.deinit();
178
+
}
160
179
}
161
180
162
181
pub fn inputToSlice(self: *App) []const u8 {
···
176
195
177
196
pub fn run(self: *App) !void {
178
197
try self.repopulateDirectory("");
179
-
180
-
var loop: vaxis.Loop(Event) = .{
181
-
.vaxis = &self.vx,
182
-
.tty = &self.tty,
183
-
};
184
-
try loop.start();
185
-
defer loop.stop();
198
+
try self.loop.start();
199
+
defer self.loop.stop();
186
200
187
201
try self.vx.enterAltScreen(self.tty.anyWriter());
188
202
try self.vx.queryTerminal(self.tty.anyWriter(), 1 * std.time.ns_per_s);
203
+
self.vx.caps.kitty_graphics = true;
189
204
190
205
while (!self.should_quit) {
191
-
loop.pollEvent();
192
-
while (loop.tryEvent()) |event| {
206
+
self.loop.pollEvent();
207
+
while (self.loop.tryEvent()) |event| {
193
208
// Global keybinds.
194
209
switch (event) {
195
210
.key_press => |key| {
···
232
247
// State specific keybinds.
233
248
switch (self.state) {
234
249
.normal => {
235
-
try EventHandlers.handleNormalEvent(self, event, &loop);
250
+
try EventHandlers.handleNormalEvent(self, event);
236
251
},
237
252
.help_menu => {
238
253
try EventHandlers.handleHelpMenuEvent(self, event);
+45
-28
src/drawer.zig
+45
-28
src/drawer.zig
···
197
197
}
198
198
if (!match) break :unsupported;
199
199
200
-
if (std.mem.eql(u8, self.last_item_path, self.current_item_path)) break :unsupported;
200
+
{
201
+
app.image.mutex.lock();
202
+
defer app.image.mutex.unlock();
201
203
202
-
var image = vaxis.zigimg.Image.fromFilePath(
203
-
app.alloc,
204
-
self.current_item_path,
205
-
) catch {
206
-
break :unsupported;
207
-
};
208
-
defer image.deinit();
204
+
if (std.mem.eql(u8, self.current_item_path, app.image.path orelse "")) {
205
+
if (app.image.data == null) break :unsupported;
209
206
210
-
if (app.vx.transmitImage(app.alloc, app.tty.anyWriter(), &image, .rgba)) |img| {
211
-
app.image = img;
212
-
} else |_| {
213
-
if (app.image) |img| {
214
-
app.vx.freeImage(app.tty.anyWriter(), img.id);
215
-
}
216
-
app.image = null;
217
-
break :unsupported;
218
-
}
219
-
220
-
if (app.image) |img| {
221
-
img.draw(preview_win, .{ .scale = .contain }) catch |err| {
222
-
const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err});
223
-
defer app.alloc.free(message);
224
-
app.notification.write(message, .err) catch {};
225
-
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
207
+
if (app.vx.transmitImage(app.alloc, app.tty.anyWriter(), &app.image.data.?, .rgba)) |img| {
208
+
img.draw(preview_win, .{ .scale = .contain }) catch |err| {
209
+
const message = try std.fmt.allocPrint(app.alloc, "Failed to draw image to screen - {}.", .{err});
210
+
defer app.alloc.free(message);
211
+
app.notification.write(message, .err) catch {};
212
+
if (app.file_logger) |file_logger| file_logger.write(message, .err) catch {};
226
213
227
-
_ = preview_win.print(&.{
228
-
.{ .text = "Failed to draw image to screen. No preview available." },
229
-
}, .{});
214
+
_ = preview_win.print(&.{
215
+
.{ .text = "Failed to draw image to screen. No preview available." },
216
+
}, .{});
217
+
};
218
+
} else |_| {
219
+
break :unsupported;
220
+
}
230
221
231
222
break :file;
232
-
};
223
+
}
224
+
225
+
const path = try app.alloc.dupe(u8, self.current_item_path);
226
+
const load_img_thread = std.Thread.spawn(.{}, loadImage, .{
227
+
app,
228
+
path,
229
+
}) catch break :unsupported;
230
+
load_img_thread.detach();
233
231
}
234
232
235
233
break :file;
···
606
604
.style = config.styles.notification.box,
607
605
}, .{ .wrap = .word });
608
606
}
607
+
608
+
fn loadImage(app: *App, path: []const u8) error{ Unsupported, OutOfMemory }!void {
609
+
const image = vaxis.zigimg.Image.fromFilePath(app.alloc, path) catch {
610
+
return error.Unsupported;
611
+
};
612
+
613
+
app.image.mutex.lock();
614
+
if (app.image.data) |data| {
615
+
var img_data = data;
616
+
img_data.deinit();
617
+
}
618
+
app.image.data = image;
619
+
620
+
if (app.image.path) |p| app.alloc.free(p);
621
+
app.image.path = path;
622
+
app.image.mutex.unlock();
623
+
624
+
app.loop.postEvent(.image_ready);
625
+
}
+4
-2
src/event_handlers.zig
+4
-2
src/event_handlers.zig
···
12
12
pub fn handleNormalEvent(
13
13
app: *App,
14
14
event: App.Event,
15
-
loop: *vaxis.Loop(App.Event),
16
15
) !void {
17
16
switch (event) {
18
17
.key_press => |key| {
···
82
81
} else {
83
82
switch (key.codepoint) {
84
83
'-', 'h', Key.left => try events.traverseLeft(app),
85
-
Key.enter, 'l', Key.right => try events.traverseRight(app, loop),
84
+
Key.enter, 'l', Key.right => try events.traverseRight(app),
86
85
'j', Key.down => app.directories.entries.next(),
87
86
'k', Key.up => app.directories.entries.previous(),
88
87
'u' => try events.undo(app),
···
90
89
}
91
90
}
92
91
},
92
+
.image_ready => {},
93
93
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
94
94
}
95
95
}
···
239
239
},
240
240
}
241
241
},
242
+
.image_ready => {},
242
243
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
243
244
}
244
245
}
···
253
254
else => {},
254
255
}
255
256
},
257
+
.image_ready => {},
256
258
.winsize => |ws| try app.vx.resize(app.alloc, app.tty.anyWriter(), ws),
257
259
}
258
260
}
+3
-3
src/events.zig
+3
-3
src/events.zig
···
391
391
}
392
392
}
393
393
394
-
pub fn traverseRight(app: *App, loop: *vaxis.Loop(App.Event)) !void {
394
+
pub fn traverseRight(app: *App) !void {
395
395
var message: ?[]const u8 = null;
396
396
defer if (message) |msg| app.alloc.free(msg);
397
397
···
421
421
if (environment.getEditor()) |editor| {
422
422
try app.vx.exitAltScreen(app.tty.anyWriter());
423
423
try app.vx.resetState(app.tty.anyWriter());
424
-
loop.stop();
424
+
app.loop.stop();
425
425
426
426
environment.openFile(app.alloc, app.directories.dir, entry.name, editor) catch |err| {
427
427
message = try std.fmt.allocPrint(app.alloc, "Failed to open file '{s}' - {}.", .{ entry.name, err });
···
429
429
if (app.file_logger) |file_logger| file_logger.write(message.?, .err) catch {};
430
430
};
431
431
432
-
try loop.start();
432
+
try app.loop.start();
433
433
try app.vx.enterAltScreen(app.tty.anyWriter());
434
434
try app.vx.enableDetectedFeatures(app.tty.anyWriter());
435
435
app.vx.queueRefresh();