Repository navigation
Expand file tree
/
Copy pathnative.zig
More file actions
5285 lines (4981 loc) · 277 KB
/
Copy pathnative.zig
File metadata and controls
5285 lines (4981 loc) · 277 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
const std = @import("std");
const sdl = @import("sdl3");
const files = @import("../files.zig");
const invocation = @import("../invocation.zig");
const save = @import("../save.zig");
const recovery = @import("../recovery.zig");
const recovery_directory = @import("../recovery_directory.zig");
const recovery_discovery = @import("../recovery_discovery.zig");
const recovery_retention = @import("../recovery_retention.zig");
const session = @import("../session.zig");
const json_merge = @import("../engine/json_merge.zig");
const zig_merge = @import("../engine/zig_merge.zig");
const repository = @import("repository.zig");
const action_registry = @import("actions.zig");
const glyph_text = @import("text.zig");
const editor = @import("editor.zig");
const dialog = @import("dialog.zig");
const ui_lsp = @import("lsp.zig");
const input_capacity = 192 * 1024;
const result_capacity = input_capacity;
const history_capacity = 768 * 1024;
const history_records = 32;
const metadata_capacity = dialog.path_capacity * 8;
const json_member_capacity = 1024;
const json_key_capacity = 16 * 1024;
const json_scanner_capacity = 8 * 1024;
// Provisional per-LoadJob ceiling: 4096 declarations/side, 64 KiB names/side, 2 MiB parser scratch.
const zig_declaration_capacity = 4096;
const zig_name_capacity = 64 * 1024;
const zig_scratch_capacity = 2 * 1024 * 1024;
const backing_capacity = input_capacity * 4 + result_capacity * 2 + history_capacity + history_records * @sizeOf(session.Record) + (input_capacity + 1) * @sizeOf(u32) * 3 + save.path_storage_capacity + save.content_capacity * 2 + metadata_capacity + recovery.Controller.ciphertext_capacity * 3 + recovery.Controller.plaintext_capacity * 3 + recovery.Controller.path_capacity * 24 + 256 * 3 + 64 * 3 + 1024 * 3 + recovery.Controller.result_capacity * 3 + 48 * 3 + @sizeOf(u16) * 1024 + 2048 + 4096 + 32 + input_capacity + 1 + multi_line_capacity * @sizeOf(@import("../engine/diff.zig").Line) * 4 + multi_cell_capacity * @sizeOf(@import("../engine/diff.zig").Cell) * 2 + multi_line_capacity * @sizeOf(@import("../engine/diff.zig").Hunk) * 2 + multi_line_capacity * @sizeOf(@import("../engine/multihunk.zig").Conflict) + json_member_capacity * @sizeOf(json_merge.Member) * 3 + json_key_capacity * 3 + json_scanner_capacity + zig_declaration_capacity * @sizeOf(zig_merge.Declaration) * 3 + zig_name_capacity * 3 + zig_scratch_capacity + @sizeOf(ui_lsp.Storage) + @sizeOf(ui_lsp.Service) + 8192;
const glyph_width = 8;
const row_height = 12;
const action_count = actions.len;
const multi_line_capacity = 128;
const multi_cell_capacity = (multi_line_capacity + 1) * (multi_line_capacity + 1);
extern "kernel32" fn GetTickCount64() callconv(.winapi) u64;
extern "kernel32" fn GetEnvironmentVariableW(name: [*:0]const u16, buffer: [*]u16, size: u32) callconv(.winapi) u32;
pub const Error = error{ ResolutionIncomplete, ProvisionalCapacityExceeded, CompiledShaderUnavailable, GraphicsUnavailable, RenderFailed, ReadOnly, ClipboardUnavailable };
const Action = action_registry.Id;
const Mode = enum { welcome, diff, merge };
const ActionInfo = struct { action: Action, name: []const u8, help: []const u8, key: sdl.SDL_Keycode, binding: []const u8 };
const actions = [_]ActionInfo{
.{ .action = .choose_base, .name = "Base", .help = "Replace whole result with merge base bytes.", .key = sdl.SDLK_B, .binding = "B" },
.{ .action = .choose_left, .name = "Left", .help = "Replace whole result with left source bytes.", .key = sdl.SDLK_L, .binding = "L" },
.{ .action = .choose_right, .name = "Right", .help = "Replace whole result with right source bytes.", .key = sdl.SDLK_R, .binding = "R" },
.{ .action = .reset, .name = "Reset", .help = "Restore initial result; reset is undoable.", .key = sdl.SDLK_ESCAPE, .binding = "Esc" },
.{ .action = .undo, .name = "Undo", .help = "Restore previous in-memory result decision.", .key = sdl.SDLK_Z, .binding = "Ctrl+Z" },
.{ .action = .redo, .name = "Redo", .help = "Restore next in-memory result decision.", .key = sdl.SDLK_Y, .binding = "Ctrl+Y" },
.{ .action = .mark_resolved, .name = "Resolve", .help = "Mark in-memory result resolved; does not save.", .key = sdl.SDLK_M, .binding = "M" },
.{ .action = .save, .name = "Save", .help = "Publish result as a durable new destination file.", .key = sdl.SDLK_S, .binding = "S" },
.{ .action = .complete, .name = "Complete", .help = "Finish only after current durable output is verified.", .key = sdl.SDLK_RETURN, .binding = "Enter" },
.{ .action = .next_change, .name = "Next change", .help = "Navigate exact bounded line-diff changes in source panes.", .key = sdl.SDLK_N, .binding = "N" },
.{ .action = .copy, .name = "Copy", .help = "Copy selected editable result bytes to clipboard.", .key = sdl.SDLK_C, .binding = "Ctrl+C" },
.{ .action = .cut, .name = "Cut", .help = "Copy selection then delete it from editable result.", .key = sdl.SDLK_X, .binding = "Ctrl+X" },
.{ .action = .paste, .name = "Paste", .help = "Insert supported clipboard bytes at result selection.", .key = sdl.SDLK_V, .binding = "Ctrl+V" },
.{ .action = .select_all, .name = "Select all", .help = "Select whole editable result.", .key = sdl.SDLK_A, .binding = "Ctrl+A" },
.{ .action = .export_recovery, .name = "Export encrypted recovery draft", .help = "Export an additional encrypted DPAPI draft; automatic recovery remains separate.", .key = sdl.SDLK_S, .binding = "Ctrl+Shift+S" },
.{ .action = .attach_result_language_server, .name = "Attach result language server", .help = "Choose exact server executable and workspace; review trust before it starts.", .key = sdl.SDLK_UNKNOWN, .binding = "Palette" },
.{ .action = .confirm_result_language_server_trust, .name = "Confirm language server trust", .help = "Start reviewed exact executable for exact workspace once.", .key = sdl.SDLK_UNKNOWN, .binding = "Palette" },
.{ .action = .detach_result_language_server, .name = "Detach language server", .help = "Stop, reap, and revoke result language server.", .key = sdl.SDLK_UNKNOWN, .binding = "Palette" },
.{ .action = .hover, .name = "Hover", .help = "Request hover at result caret from attached server.", .key = sdl.SDLK_UNKNOWN, .binding = "Palette" },
.{ .action = .symbols, .name = "Document symbols", .help = "List symbols in current result document.", .key = sdl.SDLK_UNKNOWN, .binding = "Palette" },
.{ .action = .definition, .name = "Definition", .help = "Find definition at current result caret.", .key = sdl.SDLK_UNKNOWN, .binding = "Palette" },
.{ .action = .references, .name = "References", .help = "Find references at current result caret.", .key = sdl.SDLK_UNKNOWN, .binding = "Palette" },
};
const Rect = struct {
x: f32,
y: f32,
w: f32,
h: f32,
fn contains(self: Rect, x: f32, y: f32) bool {
std.debug.assert(self.w >= 0 and self.h >= 0);
std.debug.assert(std.math.isFinite(x) and std.math.isFinite(y));
return x >= self.x and y >= self.y and x < self.x + self.w and y < self.y + self.h;
}
fn sdl_rect(self: Rect) sdl.SDL_FRect {
std.debug.assert(self.w >= 0 and self.h >= 0);
std.debug.assert(std.math.isFinite(self.x) and std.math.isFinite(self.y));
return .{ .x = self.x, .y = self.y, .w = self.w, .h = self.h };
}
};
const Layout = struct { width: f32, height: f32, buttons: [action_count]Rect, panes: [3]Rect, palette: Rect };
const Scroll = struct { line: usize = 0, column: usize = 0 };
const Model = struct {
mode: Mode,
palette_open: bool = false,
text_input_active: bool = false,
status: []const u8,
selected: usize = 0,
help_presented: ?usize = null,
query: [64]u8 = undefined,
query_len: usize = 0,
focus: usize = 1,
scroll: [3]Scroll = .{ .{}, .{}, .{} },
layout: Layout = layout_for(640, 380),
mouse_scale_x: f32 = 1,
mouse_scale_y: f32 = 1,
durable_generation: ?u64 = null,
durable_receipt: ?@import("../output.zig").Receipt = null,
observed_save_generation: ?u64 = null,
completion_job_id: ?u64 = null,
change_index: usize = 0,
completed: bool = false,
exit_requested: bool = false,
exit_ready: bool = false,
editor: ?editor.State = null,
dragging_result: bool = false,
recovery_picker: dialog.State = undefined,
lsp_picker: dialog.State = undefined,
lsp_picker_pending: bool = false,
lsp_picking_workspace: bool = false,
lsp_synced_generation: ?u64 = null,
lsp_navigation_open: bool = false,
lsp_navigation_selected: usize = 0,
lsp_text: [256]u8 = undefined,
lsp_text_len: usize = 0,
lsp_status: [64]u8 = undefined,
lsp_status_len: usize = 0,
lsp_review_identity: ?@import("../lsp/client.zig").Identity = null,
lsp_review_presented_at: u64 = 0,
lsp_review_width: f32 = 0,
lsp_review_height: f32 = 0,
lsp_confirmation_timestamp: ?u64 = null,
window: ?*sdl.SDL_Window = null,
recovery_picker_pending: bool = false,
recovery_observed: bool = false,
auto_recovery_state: ?recovery.Journal.State = null,
clipboard: ?[:0]u8 = null,
clipboard_len: usize = 0,
};
const Labels = struct { base: []const u8, left: []const u8, right: []const u8, output: []const u8 };
const Lines = struct { offsets: []u32, count: usize = 0 };
const LauncherStage = enum { idle, diff_left, diff_right, diff_output, merge_base, merge_left, merge_right, merge_output, repo_folder, repo_git, restore_recovery, restore_output };
const launcher_action_count = 6;
const LauncherAction = enum { open_two_way, open_merge, open_repository, restore_recovery, find_recovery_drafts, forget_selected_draft };
const LauncherActionInfo = struct { action: LauncherAction, name: []const u8, aliases: []const u8, help: []const u8, binding: []const u8 };
const launcher_actions = [_]LauncherActionInfo{ .{ .action = .open_two_way, .name = "Open two-way comparison", .aliases = "open diff compare two-way", .help = "Choose left and right read-only inputs, then a new output filename.", .binding = "D" }, .{ .action = .open_merge, .name = "Open three-way merge", .aliases = "open merge three-way", .help = "Choose base, left, right read-only inputs, then a new output filename.", .binding = "M" }, .{ .action = .open_repository, .name = "Open repository", .aliases = "open repo inspect", .help = "Choose repository folder and explicit installed Git executable for read-only inspection.", .binding = "R" }, .{ .action = .restore_recovery, .name = "Restore encrypted recovery", .aliases = "restore recovery draft encrypted dpapi", .help = "Choose an encrypted recovery file; originals are revalidated before its unreviewed draft opens.", .binding = "K" }, .{ .action = .find_recovery_drafts, .name = "Find recovery drafts", .aliases = "find discover recovery drafts", .help = "Scan existing protected recovery storage; select a draft to restore after choosing a new output.", .binding = "F" }, .{ .action = .forget_selected_draft, .name = "Forget selected encrypted draft", .aliases = "forget delete remove recovery draft", .help = "Request deletion of only selected encrypted generation; repeat after reviewing session and generation to confirm.", .binding = "X" } };
const DiscoveryJob = struct {
const State = enum { searching, ready, failed };
const Kind = enum { scan, forget };
io: std.Io,
mutex: std.Io.Mutex = .init,
thread: ?std.Thread = null,
state: State = .searching,
failure: anyerror = error.Unexpected,
root: [dialog.path_capacity]u8 = undefined,
root_len: usize = 0,
candidates: []recovery_discovery.Candidate,
ciphertext: []u8,
plaintext: []u8,
path_scratch: []u16,
token_scratch: []align(8) u8,
outcome: recovery_discovery.Outcome = .{},
kind: Kind = .scan,
forget_candidate: recovery_discovery.Candidate = undefined,
retention_ciphertext: []u8,
retention_plaintext: []u8,
session_path: []u8,
fn init(io: std.Io, root: []const u8, allocator: std.mem.Allocator) !DiscoveryJob {
if (root.len == 0 or root.len >= dialog.path_capacity) return error.InvalidRoot;
var job: DiscoveryJob = .{
.io = io,
.candidates = try allocator.alloc(recovery_discovery.Candidate, recovery_discovery.max_sessions),
.ciphertext = try allocator.alloc(u8, recovery_discovery.max_ciphertext),
.plaintext = try allocator.alloc(u8, recovery_discovery.max_plaintext),
.path_scratch = try allocator.alloc(u16, std.os.windows.NAME_MAX),
.token_scratch = try allocator.alignedAlloc(u8, .of(u64), 1024),
.retention_ciphertext = try allocator.alloc(u8, recovery_discovery.max_ciphertext),
.retention_plaintext = try allocator.alloc(u8, recovery_discovery.max_plaintext),
.session_path = try allocator.alloc(u8, dialog.path_capacity),
};
@memcpy(job.root[0..root.len], root);
job.root_len = root.len;
return job;
}
fn start(self: *DiscoveryJob) !void {
std.debug.assert(self.thread == null);
self.state = .searching;
self.failure = error.Unexpected;
self.outcome = .{};
self.thread = try std.Thread.spawn(.{}, worker, .{self});
}
fn restart(self: *DiscoveryJob, root: []const u8) !void {
if (root.len == 0 or root.len >= self.root.len) return error.InvalidRoot;
self.join();
if (!std.mem.eql(u8, self.root[0..self.root_len], root)) @memcpy(self.root[0..root.len], root);
self.root_len = root.len;
self.kind = .scan;
try self.start();
}
fn start_forget(self: *DiscoveryJob, candidate: recovery_discovery.Candidate) !void {
self.join();
self.kind = .forget;
self.forget_candidate = candidate;
try self.start();
}
fn worker(self: *DiscoveryJob) void {
if (self.kind == .forget) {
recovery_retention.forget(self.root[0..self.root_len], self.forget_candidate, .{
.ciphertext = self.retention_ciphertext,
.plaintext = self.retention_plaintext,
.path_scratch = self.path_scratch,
.token_scratch = self.token_scratch,
.session_path = self.session_path,
}) catch |err| {
self.finish(.failed, err, .{});
return;
};
self.finish(.ready, error.Unexpected, .{});
return;
}
const outcome = recovery_discovery.scan(self.io, self.root[0..self.root_len], self.candidates, .{
.ciphertext = self.ciphertext,
.plaintext = self.plaintext,
.path_scratch = self.path_scratch,
.token_scratch = self.token_scratch,
}) catch |err| {
self.finish(.failed, err, .{});
return;
};
self.finish(.ready, error.Unexpected, outcome);
}
fn finish(self: *DiscoveryJob, state: State, failure: anyerror, outcome: recovery_discovery.Outcome) void {
self.mutex.lockUncancelable(self.io);
self.failure = failure;
self.outcome = outcome;
self.state = state;
self.mutex.unlock(self.io);
}
fn join(self: *DiscoveryJob) void {
if (self.thread) |thread| thread.join();
self.thread = null;
}
fn deinit(self: *DiscoveryJob) void {
self.join();
}
fn view(self: *DiscoveryJob) struct { state: State, failure: anyerror, outcome: recovery_discovery.Outcome } {
self.mutex.lockUncancelable(self.io);
defer self.mutex.unlock(self.io);
return .{ .state = self.state, .failure = self.failure, .outcome = self.outcome };
}
};
const Launcher = struct {
io: std.Io,
allocator: std.mem.Allocator,
dialog_state: dialog.State,
stage: LauncherStage = .idle,
paths: [4][dialog.path_capacity]u8 = undefined,
lengths: [4]usize = .{ 0, 0, 0, 0 },
recovery_path: [dialog.path_capacity]u8 = undefined,
recovery_path_len: usize = 0,
discovery: ?DiscoveryJob = null,
discovery_selected: usize = 0,
forget_confirmation: ?recovery_discovery.Candidate = null,
selected: usize = 0,
palette_open: bool = false,
query: [64]u8 = undefined,
query_len: usize = 0,
help_presented: ?usize = null,
status: []const u8 = "Choose visible action or Ctrl+P: Open comparison, merge, repository, or encrypted recovery",
fn init(io: std.Io, allocator: std.mem.Allocator) Launcher {
return .{ .io = io, .allocator = allocator, .dialog_state = .{ .io = io } };
}
fn start(self: *Launcher, stage: LauncherStage, window: *sdl.SDL_Window) void {
std.debug.assert(self.stage == .idle);
self.stage = stage;
self.status = "Native picker open | cancellation keeps welcome unchanged";
self.dialog_state.show(if (stage == .diff_output or stage == .merge_output or stage == .restore_output) .save_file else if (stage == .repo_folder) .open_folder else .open_file, window);
}
fn start_discovery(self: *Launcher, allocator: std.mem.Allocator) !void {
var root_storage: [dialog.path_capacity]u8 = undefined;
const root_len = recovery_root(&root_storage) catch |err| {
self.status = @errorName(err);
return;
};
if (self.discovery) |*job| {
try job.restart(root_storage[0..root_len]);
} else {
var job = try DiscoveryJob.init(self.io, root_storage[0..root_len], allocator);
try job.start();
self.discovery = job;
}
self.discovery_selected = 0;
self.status = "Searching protected recovery drafts...";
}
fn accept(self: *Launcher, value: []const u8) bool {
const index = path_index(self.stage);
if (value.len == 0 or value.len > self.paths[index].len) return false;
@memcpy(self.paths[index][0..value.len], value);
self.lengths[index] = value.len;
return true;
}
fn path(self: *const Launcher, index: usize) []const u8 {
std.debug.assert(index < self.paths.len and self.lengths[index] <= self.paths[index].len);
return self.paths[index][0..self.lengths[index]];
}
};
fn path_index(stage: LauncherStage) usize {
return switch (stage) {
.diff_left, .merge_base, .repo_folder => 0,
.diff_right, .merge_left, .repo_git => 1,
.merge_right => 2,
.restore_recovery, .restore_output => 3,
.diff_output, .merge_output => 3,
.idle => unreachable,
};
}
const LspJob = struct {
const State = enum { idle, working, reviewed, started, revoked, failed };
const Operation = enum { review, confirm, close, revoke };
io: std.Io,
service: *ui_lsp.Service,
mutex: std.Io.Mutex = .init,
thread: ?std.Thread = null,
state: State = .idle,
failure: ?anyerror = null,
operation: Operation = .review,
result: [@import("../lsp/client.zig").max_document_bytes]u8 = undefined,
result_len: usize = 0,
result_generation: u64 = 0,
fn init(self: *LspJob, io: std.Io, service: *ui_lsp.Service) void {
self.* = .{ .io = io, .service = service };
}
fn start_review(self: *LspJob) !void {
try self.start(.review, "", 0);
}
fn start_confirm(self: *LspJob, result: []const u8, generation: u64) !void {
try self.start(.confirm, result, generation);
}
fn start_close(self: *LspJob) !void {
try self.start(.close, "", 0);
}
fn start_revoke(self: *LspJob) !void {
try self.start(.revoke, "", 0);
}
fn start(self: *LspJob, operation: Operation, result: []const u8, generation: u64) !void {
if (result.len > self.result.len) return error.DocumentTooLarge;
if (self.view().state == .working) return error.LspJobBusy;
self.join();
self.operation = operation;
self.result_generation = generation;
self.result_len = result.len;
@memcpy(self.result[0..result.len], result);
self.mutex.lockUncancelable(self.io);
self.state = .working;
self.failure = null;
self.mutex.unlock(self.io);
self.thread = std.Thread.spawn(.{}, worker, .{self}) catch |err| {
self.finish(.failed, err);
return err;
};
}
fn worker(self: *LspJob) void {
const outcome: State = switch (self.operation) {
.review => blk: {
_ = self.service.review(self.io) catch |err| {
self.finish(.failed, err);
return;
};
break :blk .reviewed;
},
.confirm => blk: {
self.service.confirm(self.io, self.result[0..self.result_len]) catch |err| {
self.finish(.failed, err);
return;
};
break :blk .started;
},
.close => blk: {
self.service.close();
break :blk .revoked;
},
.revoke => blk: {
self.service.revoke();
break :blk .revoked;
},
};
self.finish(outcome, null);
}
fn finish(self: *LspJob, state: State, failure: ?anyerror) void {
self.mutex.lockUncancelable(self.io);
self.state = state;
self.failure = failure;
self.mutex.unlock(self.io);
}
fn view(self: *LspJob) struct { state: State, failure: ?anyerror } {
self.mutex.lockUncancelable(self.io);
defer self.mutex.unlock(self.io);
return .{ .state = self.state, .failure = self.failure };
}
fn join(self: *LspJob) void {
if (self.thread) |thread| thread.join();
self.thread = null;
}
fn deinit(self: *LspJob) void {
self.join();
}
};
const Opened = struct {
active: session.Session,
labels: Labels,
destination: []const u8,
input_paths: save.InputPaths,
lines: [3]Lines,
save: save.Controller = undefined,
save_paths: []u8 = &.{},
save_content: []u8 = &.{},
save_preflight: []u8 = &.{},
save_initialized: bool = false,
recovery: recovery.Controller = undefined,
recovery_storage: recovery.Controller.Storage = undefined,
auto_recovery: recovery.Journal = undefined,
auto_recovery_storage: recovery.Journal.Storage = undefined,
recovery_directory: ?recovery_directory.Session = null,
recovery_root: []u8 = &.{},
recovery_directory_path: []u16 = &.{},
recovery_token: []align(8) u8 = &.{},
recovery_sddl: []u16 = &.{},
recovery_descriptor: []u8 = &.{},
recovery_name: []u8 = &.{},
clipboard: [:0]u8 = undefined,
recovery_initialized: bool = false,
auto_recovery_initialized: bool = false,
lsp: ?*ui_lsp.Service = null,
lsp_job: ?LspJob = null,
fn init_save(self: *Opened, io: std.Io) !void {
std.debug.assert(!self.save_initialized);
try self.save.init(io, self.save_paths, self.save_content, self.save_preflight);
self.save_initialized = true;
}
fn init_save_with(self: *Opened, io: std.Io, paths: []u8, content: []u8, preflight: []u8) !void {
self.save_paths = paths;
self.save_content = content;
self.save_preflight = preflight;
try self.init_save(io);
}
fn init_recovery(self: *Opened, io: std.Io) !void {
try self.recovery.init(io, self.recovery_storage);
self.recovery_initialized = true;
}
fn init_auto_recovery(self: *Opened, io: std.Io, root_override: ?[]const u8) !void {
if (@import("builtin").is_test and root_override == null) return error.TestRecoveryRootRequired;
if (comptime @import("builtin").os.tag != .windows) return error.UnsupportedPlatform;
const root_len = if (root_override) |root| blk: {
if (root.len == 0 or root.len >= self.recovery_root.len or std.mem.indexOfScalar(u8, root, 0) != null) return error.InvalidRoot;
@memcpy(self.recovery_root[0..root.len], root);
break :blk root.len;
} else blk: {
var root_w: [1024]u16 = undefined;
const root_w_len = GetEnvironmentVariableW(std.unicode.utf8ToUtf16LeStringLiteral("LOCALAPPDATA"), &root_w, root_w.len);
if (root_w_len == 0 or root_w_len >= root_w.len) return error.InvalidRoot;
const local_len = std.unicode.wtf16LeToWtf8(self.recovery_root, root_w[0..root_w_len]);
const suffix = "\\warkocz-recovery";
if (local_len == 0 or local_len + suffix.len >= self.recovery_root.len or std.mem.indexOfScalar(u8, self.recovery_root[0..local_len], 0) != null) return error.InvalidRoot;
@memcpy(self.recovery_root[local_len..][0..suffix.len], suffix);
break :blk local_len + suffix.len;
};
const owned = try recovery_directory.create(io, self.recovery_root[0..root_len], self.recovery_directory_path, self.recovery_token, self.recovery_sddl, self.recovery_descriptor, self.recovery_name);
self.recovery_directory = owned;
errdefer {
var directory = self.recovery_directory.?;
recovery_directory.destroy(&directory) catch {};
self.recovery_directory = null;
}
self.auto_recovery = try recovery.Journal.init(io, &self.recovery_directory.?, self.auto_recovery_storage, recovery_clock);
self.auto_recovery_initialized = true;
}
fn start_auto_recovery(self: *Opened) !void {
std.debug.assert(self.auto_recovery_initialized);
try self.auto_recovery.start();
}
fn deinit(self: *Opened) void {
if (self.lsp_job) |*job| job.deinit();
if (self.lsp) |service| service.revoke();
if (self.auto_recovery_initialized) self.auto_recovery.deinit();
if (self.recovery_directory) |*directory| recovery_directory.destroy(directory) catch {};
if (self.recovery_initialized) self.recovery.deinit();
if (self.save_initialized) self.save.deinit();
self.auto_recovery_initialized = false;
self.recovery_directory = null;
self.recovery_initialized = false;
self.save_initialized = false;
}
};
const LaunchResult = union(enum) { request: invocation.Request, opened: Opened };
pub fn run(init: std.process.Init, request: invocation.Request) !void {
std.debug.assert(request == .bootstrap or request == .diff or request == .merge or request == .repo);
const backing = try std.heap.page_allocator.alloc(u8, backing_capacity);
defer std.heap.page_allocator.free(backing);
if (request == .repo) return repository.run(init, request.repo);
var allocator = std.heap.FixedBufferAllocator.init(backing);
const window = try create_window();
defer sdl.SDL_Quit();
defer sdl.SDL_DestroyWindow(window);
const device = try create_device();
defer sdl.SDL_DestroyGPUDevice(device);
const renderer = sdl.SDL_CreateGPURenderer(device, window) orelse return graphics_failure("SDL_CreateGPURenderer failed");
defer sdl.SDL_DestroyRenderer(renderer);
var restored = false;
var scene_request = request;
var launcher = Launcher.init(init.io, allocator.allocator());
defer if (launcher.discovery) |*job| job.deinit();
var opened: Opened = undefined;
if (request == .bootstrap) switch ((try launcher_event_loop(renderer, window, &launcher, allocator.allocator())) orelse return) {
.request => |value| scene_request = value,
.opened => |value| {
opened = value;
restored = true;
},
};
if (!restored) {
if (scene_request == .repo) return repository.run_initialized(init, scene_request.repo);
opened = (try load_session(renderer, window, init.io, scene_request, allocator.allocator())) orelse return;
}
try opened.init_save(init.io);
try opened.init_recovery(init.io);
if (opened.auto_recovery_initialized) opened.start_auto_recovery() catch {
opened.auto_recovery_initialized = false;
if (opened.recovery_directory) |*directory| recovery_directory.destroy(directory) catch {};
opened.recovery_directory = null;
};
const automatic_recovery_ready = opened.auto_recovery_initialized;
defer opened.deinit();
var model = Model{ .mode = if (opened.active.mode == .two_way) .diff else .merge, .status = "Ctrl+P actions | ASCII diagnostic byte-view, not editor", .recovery_picker = .{ .io = init.io }, .lsp_picker = .{ .io = init.io }, .window = window, .clipboard = opened.clipboard };
model.editor = editor.State.init(opened.active.result());
model.status = if (restored) "RECOVERY RESTORED | originals revalidated | unresolved and unreviewed | reconcile manually before Save" else if (!automatic_recovery_ready) "RECOVERY UNPROTECTED | automatic encrypted recovery unavailable | editor remains usable" else if (model.editor == null) "READ-ONLY BYTE REVIEW | recovery ready | unsupported control, binary, or non-ASCII bytes remain exact review" else "EDITABLE ASCII RESULT | CRLF preserved | automatic recovery ready";
try event_loop(renderer, window, &model, &opened);
}
fn create_window() Error!*sdl.SDL_Window {
if (!sdl.SDL_Init(sdl.SDL_INIT_VIDEO)) return graphics_failure("SDL video initialization failed");
errdefer sdl.SDL_Quit();
const flags = sdl.SDL_WINDOW_RESIZABLE | sdl.SDL_WINDOW_HIGH_PIXEL_DENSITY;
const window = sdl.SDL_CreateWindow("warkocz native byte-view", 1280, 760, flags) orelse return graphics_failure("SDL_CreateWindow failed");
errdefer sdl.SDL_DestroyWindow(window);
if (!sdl.SDL_SetWindowMinimumSize(window, 960, 640)) return graphics_failure("SDL_SetWindowMinimumSize failed");
return window;
}
fn create_device() Error!*sdl.SDL_GPUDevice {
if (@import("builtin").os.tag == .macos) return graphics_failure("macOS needs bundled metallib shaders; runtime MSL is unsupported");
const formats = sdl.SDL_GPU_SHADERFORMAT_DXIL | sdl.SDL_GPU_SHADERFORMAT_SPIRV;
return sdl.SDL_CreateGPUDevice(formats, false, null) orelse graphics_failure("No compatible DXIL or SPIR-V GPU device");
}
fn launcher_event_loop(renderer: *sdl.SDL_Renderer, window: *sdl.SDL_Window, launcher: *Launcher, allocator: std.mem.Allocator) !?LaunchResult {
var model = Model{ .mode = .welcome, .status = launcher.status };
while (true) {
if (try consume_discovery(launcher, window)) |value| {
const opened = (try load_recovery_session(renderer, window, launcher.io, value.recovery, value.output, allocator)) orelse return null;
return .{ .opened = opened };
}
if (try launcher_recovery_path(launcher, window)) |value| {
const opened = (try load_recovery_session(renderer, window, launcher.io, value.recovery, value.output, allocator)) orelse return null;
return .{ .opened = opened };
}
if (launcher.stage != .restore_recovery and launcher.stage != .restore_output) {
if (try launcher_result(launcher, window)) |request| return .{ .request = request };
}
model.status = launcher.status;
try update_layout(renderer, window, &model);
try render_launcher(renderer, &model, launcher);
launcher.help_presented = if (!launcher.palette_open or launcher_matches(launcher, launcher.selected)) launcher.selected else null;
var event: sdl.SDL_Event = undefined;
if (!sdl.SDL_WaitEventTimeout(&event, 50)) continue;
if (event.type == sdl.SDL_EVENT_QUIT or event.type == sdl.SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
if (launcher.stage == .idle) return null;
launcher.status = "Picker still pending | dismiss picker before closing; callback state remains alive";
continue;
}
try handle_launcher_event(&event, window, &model, launcher);
}
}
fn recovery_root(buffer: []u8) !usize {
if (comptime @import("builtin").os.tag != .windows) return error.UnsupportedPlatform;
var local: [1024]u16 = undefined;
const length = GetEnvironmentVariableW(std.unicode.utf8ToUtf16LeStringLiteral("LOCALAPPDATA"), &local, local.len);
if (length == 0 or length >= local.len) return error.RecoveryRootUnavailable;
const used = std.unicode.wtf16LeToWtf8(buffer, local[0..length]);
const suffix = "\\warkocz-recovery";
if (used == 0 or used + suffix.len >= buffer.len) return error.RecoveryRootUnavailable;
@memcpy(buffer[used..][0..suffix.len], suffix);
return used + suffix.len;
}
fn request_forget_selected_draft(launcher: *Launcher) void {
const job = if (launcher.discovery) |*value| value else return;
const view = job.view();
if (view.state != .ready or job.kind != .scan or launcher.discovery_selected >= view.outcome.count) {
launcher.status = "Select a discovered encrypted draft before forgetting";
return;
}
const selected = job.candidates[launcher.discovery_selected];
if (launcher.forget_confirmation) |candidate| {
if (!same_candidate(candidate, selected)) {
launcher.forget_confirmation = null;
launcher.status = "Selection changed | Forget confirmation cancelled; no files changed";
return;
}
launcher.forget_confirmation = null;
job.start_forget(selected) catch |err| {
launcher.status = @errorName(err);
return;
};
launcher.status = "Forgetting selected encrypted draft generation...";
return;
}
launcher.forget_confirmation = selected;
launcher.status = "Confirm Forget | selected session and generation shown below; X again confirms, Esc cancels";
}
fn same_candidate(left: recovery_discovery.Candidate, right: recovery_discovery.Candidate) bool {
return left.path_len == right.path_len and left.generation == right.generation and std.mem.eql(u8, left.path[0..left.path_len], right.path[0..right.path_len]) and std.mem.eql(u8, &left.ciphertext_digest, &right.ciphertext_digest);
}
fn select_discovery_candidate(launcher: *Launcher, window: *sdl.SDL_Window) void {
const job = if (launcher.discovery) |*value| value else return;
const view = job.view();
if (view.state != .ready or launcher.discovery_selected >= view.outcome.count) return;
const candidate = job.candidates[launcher.discovery_selected];
@memcpy(launcher.recovery_path[0..candidate.path_len], candidate.path[0..candidate.path_len]);
launcher.recovery_path_len = candidate.path_len;
launcher.start(.restore_output, window);
launcher.status = "Draft selected | choose a new output; originals revalidated before restore";
}
fn consume_discovery(launcher: *Launcher, window: *sdl.SDL_Window) !?RecoveryLaunch {
const job = if (launcher.discovery) |*value| value else return null;
const view = job.view();
if (view.state == .searching) return null;
if (job.kind == .forget) {
if (view.state == .ready) {
const root = job.root[0..job.root_len];
job.restart(root) catch |err| {
launcher.status = @errorName(err);
return null;
};
launcher.status = "Draft forgotten | refreshing protected recovery discovery...";
} else {
launcher.status = @errorName(view.failure);
job.restart(job.root[0..job.root_len]) catch {};
}
return null;
}
if (view.state == .failed) {
launcher.status = if (view.failure == error.FileNotFound or view.failure == error.PathAlreadyExists) "No recovery drafts found" else @errorName(view.failure);
return null;
}
if (view.outcome.count == 0) {
launcher.status = if (view.outcome.skipped == 0) "No recovery drafts found" else "No valid recovery drafts found | corrupt or foreign records skipped";
} else if (view.outcome.truncated or view.outcome.resource_limited) {
launcher.status = "Recovery results bounded | more drafts may exist; select listed draft or retry";
} else {
launcher.status = "Recovery drafts found | arrows select, Enter restores to new output";
}
if (launcher.stage == .restore_output) return launcher_recovery_path(launcher, window);
return null;
}
fn launcher_matches(launcher: *const Launcher, index: usize) bool {
std.debug.assert(index < launcher_actions.len and launcher.query_len <= launcher.query.len);
const action = launcher_actions[index];
const query = launcher.query[0..launcher.query_len];
return std.ascii.findIgnoreCase(action.name, query) != null or std.ascii.findIgnoreCase(action.aliases, query) != null or std.ascii.findIgnoreCase(action.help, query) != null;
}
fn launcher_disabled_reason(launcher: *const Launcher, index: usize) ?[]const u8 {
std.debug.assert(index < launcher_actions.len);
return if (launcher.stage == .idle) null else "Native file picker is pending; dismiss it before another open action.";
}
fn request_launcher_action(index: usize, window: *sdl.SDL_Window, launcher: *Launcher) void {
std.debug.assert(index < launcher_actions.len);
launcher.selected = index;
if (launcher_disabled_reason(launcher, index)) |reason| {
launcher.status = reason;
return;
}
if (launcher.help_presented != index) {
launcher.status = "Read selected action help; repeat click/key or Enter to open picker.";
return;
}
const stage: LauncherStage = switch (launcher_actions[index].action) {
.open_two_way => .diff_left,
.open_merge => .merge_base,
.open_repository => .repo_folder,
.restore_recovery => .restore_recovery,
.find_recovery_drafts, .forget_selected_draft => .idle,
};
if (launcher_actions[index].action == .find_recovery_drafts) {
launcher.start_discovery(launcher.allocator) catch |err| {
launcher.status = @errorName(err);
};
return;
}
if (launcher_actions[index].action == .forget_selected_draft) {
request_forget_selected_draft(launcher);
return;
}
launcher.start(stage, window);
}
fn handle_launcher_event(event: *const sdl.SDL_Event, window: *sdl.SDL_Window, model: *Model, launcher: *Launcher) !void {
if (event.type == sdl.SDL_EVENT_WINDOW_RESIZED or event.type == sdl.SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED or event.type == sdl.SDL_EVENT_WINDOW_EXPOSED) return;
if (event.type == sdl.SDL_EVENT_TEXT_INPUT and launcher.palette_open) {
for (std.mem.span(event.text.text)) |byte| {
if (launcher.query_len == launcher.query.len) break;
if (byte >= 32 and byte <= 126) {
launcher.query[launcher.query_len] = byte;
launcher.query_len += 1;
}
}
for (launcher_actions, 0..) |_, index| if (launcher_matches(launcher, index)) {
launcher.selected = index;
break;
};
return;
}
if (event.type == sdl.SDL_EVENT_MOUSE_BUTTON_DOWN and event.button.button == sdl.SDL_BUTTON_LEFT and launcher.stage == .idle) {
const x = event.button.x * model.mouse_scale_x;
const y = event.button.y * model.mouse_scale_y;
if (launcher.discovery) |*job| {
const view = job.view();
if (view.state == .ready and view.outcome.count > 0) {
const first_row = model.layout.buttons[launcher_action_count - 1].y + 48;
if (x >= 4 and y >= first_row) {
const row: usize = @intFromFloat(@floor((y - first_row) / 12));
if (row < view.outcome.count and row < 8) {
if (row != launcher.discovery_selected and launcher.forget_confirmation != null) {
launcher.forget_confirmation = null;
launcher.status = "Selection changed | Forget confirmation cancelled; no files changed";
}
launcher.discovery_selected = row;
return;
}
}
}
}
for (model.layout.buttons[0..launcher_action_count], 0..) |rect, index| if (rect.contains(x, y)) {
request_launcher_action(index, window, launcher);
return;
};
return;
}
if (event.type != sdl.SDL_EVENT_KEY_DOWN) return;
const key = event.key.key;
if ((event.key.mod & sdl.SDL_KMOD_CTRL) != 0 and key == sdl.SDLK_P) {
launcher.palette_open = !launcher.palette_open;
launcher.query_len = 0;
return;
}
if (launcher.palette_open) {
if (key == sdl.SDLK_ESCAPE) {
launcher.palette_open = false;
if (launcher.forget_confirmation != null) {
launcher.forget_confirmation = null;
launcher.status = "Forget cancelled | no files changed";
}
} else if (key == sdl.SDLK_BACKSPACE) launcher.query_len -|= 1 else if (key == sdl.SDLK_RETURN and launcher_matches(launcher, launcher.selected)) request_launcher_action(launcher.selected, window, launcher) else if (key == sdl.SDLK_UP or key == sdl.SDLK_DOWN) {
const delta: usize = if (key == sdl.SDLK_DOWN) 1 else launcher_actions.len - 1;
launcher.selected = (launcher.selected + delta) % launcher_actions.len;
}
return;
}
if (launcher.stage != .idle) return;
if (key == sdl.SDLK_ESCAPE and launcher.forget_confirmation != null) {
launcher.forget_confirmation = null;
launcher.status = "Forget cancelled | no files changed";
return;
}
if (launcher.discovery) |*job| {
const view = job.view();
if (view.state == .ready and view.outcome.count > 0) {
if (key == sdl.SDLK_UP or key == sdl.SDLK_DOWN) {
const next = if (key == sdl.SDLK_UP) launcher.discovery_selected -| 1 else @min(view.outcome.count - 1, launcher.discovery_selected + 1);
if (next != launcher.discovery_selected and launcher.forget_confirmation != null) {
launcher.forget_confirmation = null;
launcher.status = "Selection changed | Forget confirmation cancelled; no files changed";
}
launcher.discovery_selected = next;
} else if (key == sdl.SDLK_RETURN) select_discovery_candidate(launcher, window) else if (key == sdl.SDLK_X) request_forget_selected_draft(launcher);
return;
}
}
const index: ?usize = switch (key) {
sdl.SDLK_D => 0,
sdl.SDLK_M => 1,
sdl.SDLK_R => 2,
sdl.SDLK_K => 3,
sdl.SDLK_F => 4,
else => null,
};
if (index) |value| request_launcher_action(value, window, launcher);
}
const RecoveryLaunch = struct { recovery: []const u8, output: []const u8 };
fn launcher_recovery_path(launcher: *Launcher, window: *sdl.SDL_Window) !?RecoveryLaunch {
if (launcher.stage != .restore_recovery and launcher.stage != .restore_output) return null;
switch (launcher.dialog_state.take(launcher.io)) {
.pending => return null,
.cancelled => {
launcher.stage = .idle;
launcher.status = "RECOVERY RESTORE CANCELLED | welcome unchanged";
return null;
},
.failed => {
launcher.stage = .idle;
launcher.status = "RECOVERY PICKER FAILED | welcome unchanged";
return null;
},
.selected => |path| {
if (launcher.stage == .restore_recovery) {
if (path.len == 0 or path.len > launcher.recovery_path.len) {
launcher.stage = .idle;
launcher.status = "Recovery path exceeds bounded capacity | welcome unchanged";
return null;
}
@memcpy(launcher.recovery_path[0..path.len], path);
launcher.recovery_path_len = path.len;
launcher.stage = .idle;
launcher.start(.restore_output, window);
launcher.status = "RECOVERY FILE SELECTED | choose a new output filename; encrypted file remains untouched and existing files are never replaced";
return null;
}
if (!launcher.accept(path)) {
launcher.stage = .idle;
launcher.status = "Output path exceeds bounded capacity | welcome unchanged";
return null;
}
launcher.stage = .idle;
return .{ .recovery = launcher.recovery_path[0..launcher.recovery_path_len], .output = launcher.path(3) };
},
}
}
fn launcher_result(launcher: *Launcher, window: *sdl.SDL_Window) !?invocation.Request {
switch (launcher.dialog_state.take(launcher.io)) {
.pending => return null,
.cancelled => {
if (launcher.stage != .idle) launcher.status = "Picker cancelled | welcome unchanged";
launcher.stage = .idle;
return null;
},
.failed => {
launcher.stage = .idle;
launcher.status = "Picker failed | welcome unchanged";
return null;
},
.selected => |path| if (!launcher.accept(path)) {
launcher.stage = .idle;
launcher.status = "Picker path exceeds bounded capacity | welcome unchanged";
return null;
},
}
return advance_launcher(launcher, window);
}
fn advance_launcher(launcher: *Launcher, window: *sdl.SDL_Window) ?invocation.Request {
const next: ?LauncherStage = switch (launcher.stage) {
.diff_left => .diff_right,
.diff_right => .diff_output,
.merge_base => .merge_left,
.merge_left => .merge_right,
.merge_right => .merge_output,
.repo_folder => .repo_git,
.diff_output, .merge_output, .repo_git, .restore_recovery, .restore_output, .idle => null,
};
if (next) |stage| {
launcher.stage = .idle;
launcher.start(stage, window);
return null;
}
const request: invocation.Request = switch (launcher.stage) {
.diff_output => .{ .diff = .{ .left = launcher.path(0), .right = launcher.path(1), .output = launcher.path(3), .left_label = null, .right_label = null, .output_label = null, .wait = false } },
.merge_output => .{ .merge = .{ .base = launcher.path(0), .left = launcher.path(1), .right = launcher.path(2), .output = launcher.path(3), .base_label = null, .left_label = null, .right_label = null, .output_label = null, .wait = false } },
.repo_git => .{ .repo = .{ .path = launcher.path(0), .git = launcher.path(1), .history_limit = 32 } },
else => unreachable,
};
launcher.stage = .idle;
return request;
}
fn graphics_failure(summary: []const u8) Error {
std.debug.assert(summary.len > 0);
std.debug.assert(summary.len < 512);
var buffer: [1536]u8 = undefined;
const detail = std.mem.span(sdl.SDL_GetError());
const message = std.fmt.bufPrintSentinel(&buffer, "{s}\nSDL: {s}{s}\nUpdate GPU driver or use a compatible GPU. No input files opened during graphics initialization.", .{ summary, detail[0..@min(detail.len, 512)], if (detail.len > 512) " [detail clipped; inspect stderr]" else "" }, 0) catch unreachable;
std.debug.print("SDL detail: {s}\n", .{detail});
std.debug.print("warkocz: {s}\n", .{message});
_ = sdl.SDL_ShowSimpleMessageBox(sdl.SDL_MESSAGEBOX_ERROR, "warkocz graphics unavailable", message, null);
return if (@import("builtin").os.tag == .macos) error.CompiledShaderUnavailable else error.GraphicsUnavailable;
}
fn show_recovery_load_failure(err: anyerror) void {
var buffer: [512]u8 = undefined;
const message = std.fmt.bufPrintSentinel(&buffer, "Recovery was not restored: {s}.\nOriginal input changed or is unavailable, encrypted recovery file remains untouched. Reconcile original paths then retry.", .{@errorName(err)}, 0) catch unreachable;
_ = sdl.SDL_ShowSimpleMessageBox(sdl.SDL_MESSAGEBOX_ERROR, "warkocz recovery requires reconciliation", message, null);
}
fn show_load_failure(err: anyerror) void {
var buffer: [512]u8 = undefined;
const message = std.fmt.bufPrintSentinel(&buffer, "Cannot open file session: {s}.\nCheck literal paths and permissions. Each input/result is limited to 192 KiB in this provisional slice. No file was written.", .{@errorName(err)}, 0) catch unreachable;
_ = sdl.SDL_ShowSimpleMessageBox(sdl.SDL_MESSAGEBOX_ERROR, "warkocz input unavailable", message, null);
}
fn mode_for_request(request: invocation.Request) Mode {
std.debug.assert(request != .help and request != .version and request != .repo);
return switch (request) {
.bootstrap => .welcome,
.diff => .diff,
.merge => .merge,
.help, .version, .repo => unreachable,
};
}
fn recovery_storage(allocator: std.mem.Allocator) !recovery.Controller.Storage {
return .{
.destination = try allocator.alloc(u8, recovery.Controller.path_capacity),
.paths = .{ try allocator.alloc(u8, recovery.Controller.path_capacity), try allocator.alloc(u8, recovery.Controller.path_capacity), try allocator.alloc(u8, recovery.Controller.path_capacity) },
.engine = try allocator.alloc(u8, 256),
.engine_version = try allocator.alloc(u8, 64),
.provenance = try allocator.alloc(u8, 1024),
.result = try allocator.alloc(u8, recovery.Controller.result_capacity),
.plaintext = try allocator.alloc(u8, recovery.Controller.plaintext_capacity),
.ciphertext = try allocator.alloc(u8, recovery.Controller.ciphertext_capacity),
.publish_scratch = try allocator.alloc(u8, 48),
};
}
fn recovery_clock() u64 {
return GetTickCount64() * std.time.ns_per_ms;
}
const OpenStorage = struct {
base: []u8,
left: []u8,
right: []u8,
recovery_storage: recovery.Controller.Storage,
auto_recovery_storage: recovery.Journal.Storage,
recovery_root: []u8,
recovery_directory_path: []u16,
recovery_token: []align(8) u8,
recovery_sddl: []u16,
recovery_descriptor: []u8,
recovery_name: []u8,
clipboard: [:0]u8,
session_storage: session.Storage,
save_paths: []u8,
save_content: []u8,
save_preflight: []u8,
metadata: []u8,
job_recovery_path: []u8,
job_recovery_output: []u8,
job_paths: [4][]u8,