Skip to content

Commit 5b6bd84

Browse files
authored
feat: reload indicator (#105)
* feat: add reload indicator timer service * feat: add reload indicator setting and status bar item * feat: integrate reload indicator into render flow * docs: explain reload indicator options
1 parent f304c59 commit 5b6bd84

4 files changed

Lines changed: 176 additions & 8 deletions

File tree

docs/config.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ Below is an example configuration file that replicates the default settings. You
3232
},
3333
"FileMonitor": {
3434
"enabled": true,
35-
"latency": 0.1
35+
"latency": 0.1,
36+
"reload_indicator_duration": 1.0
3637
},
3738
"General": {
3839
"colorize": false,
@@ -52,7 +53,9 @@ Below is an example configuration file that replicates the default settings. You
5253
"items": [
5354
" ",
5455
{ "view": { "text": "VIS" }, "command": { "text": "CMD" } },
55-
" <path><separator><page>:<total_pages> "
56+
" <path> ",
57+
{ "idle": { "text": " " }, "reload": { "text": "*" } },
58+
"<separator><page>:<total_pages> "
5659
]
5760
},
5861
"Cache": {
@@ -80,6 +83,7 @@ The rest of this reference provides detailed explanations for each configuration
8083
- [Plain Items](#plain-items)
8184
- [Styled Items](#styled-items)
8285
- [Mode-aware Items](#mode-aware-items)
86+
- [Reload-aware Items](#reload-aware-items)
8387
- [Cache](#cache)
8488

8589
---
@@ -165,6 +169,7 @@ The `FileMonitor` section controls the automatic reloading feature, useful for l
165169
| :--- | :--- | :--- |
166170
| `enabled` | Boolean | Enables file change detection and automatic reloading |
167171
| `latency` | Float (seconds) | The time interval between checking for changes |
172+
| `reload_indicator_duration` | Float (seconds) | How long the reload indicator remains visible (`0.0` disables it) |
168173

169174
---
170175

@@ -207,7 +212,7 @@ The `StatusBar` section controls the information shown at the bottom of the wind
207212
| :--- | :--- | :--- |
208213
| `enabled` | Boolean | Enables the status bar |
209214
| `style` | [Style](#style) | Default appearance of the entire status bar |
210-
| `items` | [Items](#items) | Each item is a part of the status bar |
215+
| `items` | [Items](#items) | Status bar items |
211216

212217
### Style
213218

@@ -240,12 +245,12 @@ The `ul_style` property can be set to one of the following styles:
240245

241246
### Items
242247

243-
There are three kinds of items:
248+
The `items` property can be set to an array of status bar items, which include:
244249

245250
* **[plain items](#plain-items)**: text with default styling
246251
* **[styled items](#styled-items)**: text with custom [styling](#style)
247252
* **[mode-aware items](#mode-aware-items)**: styled items to be displayed depending on the current mode
248-
253+
* **[reload-aware items](#reload-aware-items)**: styled items to be displayed depending on the current reload indicator state
249254
#### Plain Items
250255

251256
Plain items are just strings that may include placeholders (e.g., `<page>:<total_pages>`). These placeholders are replaced with dynamic content at runtime.
@@ -292,8 +297,17 @@ Mode-aware items switch their content based on the current mode. Each item must
292297
}
293298
```
294299

300+
#### Reload-aware Items
301+
302+
Reload-aware items switch their content based on the reload indicator state. Each item must include at least one of:
303+
304+
| Property | Type | Description |
305+
| :--- | :--- | :--- |
306+
| `idle` | [Styled item](#styled-items) | Item to display when the reload indicator is off |
307+
| `reload` | [Styled item](#styled-items) | Item to display when the reload indicator is on |
308+
295309
>[!TIP]
296-
>Try using placeholders in mode-aware items to enhance feedback!
310+
>Try using placeholders in mode- and reload-aware items to enhance feedback!
297311
298312
---
299313

src/Context.zig

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,21 @@ const fzwatch = @import("fzwatch");
66
const Config = @import("config/Config.zig");
77
const DocumentHandler = @import("handlers/DocumentHandler.zig");
88
const Cache = @import("./Cache.zig");
9+
const ReloadIndicatorTimer = @import("services/ReloadIndicatorTimer.zig");
910

1011
pub const panic = vaxis.panic_handler;
1112

12-
const Event = union(enum) {
13+
pub const Event = union(enum) {
1314
key_press: vaxis.Key,
1415
mouse: vaxis.Mouse,
1516
winsize: vaxis.Winsize,
1617
file_changed,
18+
reload_done: usize,
1719
};
1820

1921
pub const ModeType = enum { view, command };
2022
pub const Mode = union(ModeType) { view: ViewMode, command: CommandMode };
23+
pub const ReloadIndicatorState = enum { idle, reload };
2124

2225
pub const Context = struct {
2326
const Self = @This();
@@ -38,6 +41,8 @@ pub const Context = struct {
3841
reload_page: bool,
3942
cache: Cache,
4043
should_check_cache: bool,
44+
reload_indicator_timer: ReloadIndicatorTimer,
45+
current_reload_indicator_state: ReloadIndicatorState,
4146
buf: []u8,
4247

4348
pub fn init(allocator: std.mem.Allocator, args: [][:0]u8) !Self {
@@ -64,6 +69,7 @@ pub const Context = struct {
6469
const vx = try vaxis.init(allocator, .{});
6570
const buf = try allocator.alloc(u8, 4096);
6671
const tty = try vaxis.Tty.init(buf);
72+
const reload_indicator_timer = ReloadIndicatorTimer.init(config);
6773

6874
return .{
6975
.allocator = allocator,
@@ -82,6 +88,8 @@ pub const Context = struct {
8288
.reload_page = true,
8389
.cache = Cache.init(allocator, config, vx, &tty),
8490
.should_check_cache = config.cache.enabled,
91+
.reload_indicator_timer = reload_indicator_timer,
92+
.current_reload_indicator_state = .idle,
8593
.buf = buf,
8694
};
8795
}
@@ -102,6 +110,7 @@ pub const Context = struct {
102110
self.config.deinit();
103111
self.allocator.destroy(self.config);
104112
self.arena.deinit();
113+
self.reload_indicator_timer.deinit();
105114
self.cache.deinit();
106115
self.document_handler.deinit();
107116
self.vx.deinit(self.allocator, self.tty.writer());
@@ -142,6 +151,9 @@ pub const Context = struct {
142151
w.setCallback(callback, &loop);
143152
self.watcher_thread = try std.Thread.spawn(.{}, watcherWorker, .{ self, w });
144153
}
154+
if (self.config.status_bar.enabled and self.config.file_monitor.reload_indicator_duration > 0) {
155+
try self.reload_indicator_timer.start(&loop);
156+
}
145157
}
146158

147159
while (!self.should_quit) {
@@ -204,6 +216,13 @@ pub const Context = struct {
204216
try self.document_handler.reloadDocument();
205217
self.cache.clear();
206218
self.reload_page = true;
219+
if (self.config.status_bar.enabled and self.config.file_monitor.reload_indicator_duration > 0) {
220+
self.current_reload_indicator_state = .reload;
221+
self.reload_indicator_timer.notifyChange();
222+
}
223+
},
224+
.reload_done => {
225+
self.current_reload_indicator_state = .idle;
207226
},
208227
}
209228
}
@@ -318,6 +337,12 @@ pub const Context = struct {
318337
.command => try expandPlaceholders(&expanded_items, mode_aware.command),
319338
}
320339
},
340+
.reload_aware => |reload_aware| {
341+
switch (self.current_reload_indicator_state) {
342+
.reload => try expandPlaceholders(&expanded_items, reload_aware.reload),
343+
.idle => try expandPlaceholders(&expanded_items, reload_aware.idle),
344+
}
345+
},
321346
}
322347
}
323348

@@ -393,7 +418,7 @@ pub const Context = struct {
393418
if (std.mem.startsWith(u8, full_path, cwd)) {
394419
var path = full_path[cwd.len..];
395420
if (path.len > 0 and path[0] == '/') path = path[1..];
396-
text = try std.fmt.allocPrint(allocator, "./{s}", .{path});
421+
text = try std.fmt.allocPrint(allocator, "{s}", .{path}); // trim cwd
397422
} else if (std.posix.getenv("HOME")) |home| {
398423
if (std.mem.startsWith(u8, full_path, home)) {
399424
var path = full_path[home.len..];

src/config/Config.zig

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,15 @@ pub const FileMonitor = struct {
3434
enabled: bool = true,
3535
// Amount of time in seconds to wait in between polling for file changes
3636
latency: f16 = 0.1,
37+
reload_indicator_duration: f16 = 1.0,
3738

3839
pub fn parse(val: std.json.Value, allocator: std.mem.Allocator) FileMonitor {
3940
var file_monitor = FileMonitor{};
4041
if (val != .object) return file_monitor;
4142

4243
file_monitor.enabled = parseType(bool, val.object, "enabled", allocator, file_monitor.enabled);
4344
file_monitor.latency = parseType(f16, val.object, "latency", allocator, file_monitor.latency);
45+
file_monitor.reload_indicator_duration = parseType(f16, val.object, "reload_indicator_duration", allocator, file_monitor.reload_indicator_duration);
4446

4547
return file_monitor;
4648
}
@@ -102,9 +104,14 @@ pub const StatusBar = struct {
102104
view: StyledItem,
103105
command: StyledItem,
104106
};
107+
pub const ReloadAwareItem = struct {
108+
idle: StyledItem,
109+
reload: StyledItem,
110+
};
105111
pub const Item = union(enum) {
106112
styled: StyledItem,
107113
mode_aware: ModeAwareItem,
114+
reload_aware: ReloadAwareItem,
108115
};
109116

110117
const default_style = vaxis.Cell.Style{
@@ -126,6 +133,10 @@ pub const StatusBar = struct {
126133
.{ .styled = .{ .text = " ", .style = default_style } },
127134
.{ .styled = .{ .text = PATH, .style = default_style } },
128135
.{ .styled = .{ .text = " ", .style = default_style } },
136+
.{ .reload_aware = .{
137+
.idle = .{ .text = " ", .style = default_style },
138+
.reload = .{ .text = "*", .style = default_style },
139+
} },
129140
.{ .styled = .{ .text = SEPARATOR, .style = default_style } },
130141
.{ .styled = .{ .text = PAGE, .style = default_style } },
131142
.{ .styled = .{ .text = ":", .style = default_style } },
@@ -325,6 +336,10 @@ fn applyStyle(item: StatusBar.Item, style: vaxis.Cell.Style, allocator: std.mem.
325336
.view = StatusBar.StyledItem{ .text = "", .style = style },
326337
.command = StatusBar.StyledItem{ .text = "", .style = style },
327338
} };
339+
var reload_aware_item: StatusBar.Item = .{ .reload_aware = StatusBar.ReloadAwareItem{
340+
.idle = StatusBar.StyledItem{ .text = "", .style = style },
341+
.reload = StatusBar.StyledItem{ .text = "", .style = style },
342+
} };
328343

329344
switch (item) {
330345
.styled => |styled| {
@@ -336,6 +351,11 @@ fn applyStyle(item: StatusBar.Item, style: vaxis.Cell.Style, allocator: std.mem.
336351
mode_aware_item.mode_aware.command.text = allocator.dupe(u8, mode_aware.command.text) catch mode_aware_item.mode_aware.command.text;
337352
return mode_aware_item;
338353
},
354+
.reload_aware => |reload_aware| {
355+
reload_aware_item.reload_aware.reload.text = allocator.dupe(u8, reload_aware.reload.text) catch reload_aware_item.reload_aware.reload.text;
356+
reload_aware_item.reload_aware.idle.text = allocator.dupe(u8, reload_aware.idle.text) catch reload_aware_item.reload_aware.idle.text;
357+
return reload_aware_item;
358+
},
339359
}
340360
}
341361

@@ -345,6 +365,10 @@ fn parseItem(val: std.json.Value, allocator: std.mem.Allocator, fallback_style:
345365
.view = StatusBar.StyledItem{ .text = "", .style = fallback_style },
346366
.command = StatusBar.StyledItem{ .text = "", .style = fallback_style },
347367
} };
368+
var reload_aware_item: StatusBar.Item = .{ .reload_aware = StatusBar.ReloadAwareItem{
369+
.idle = StatusBar.StyledItem{ .text = "", .style = fallback_style },
370+
.reload = StatusBar.StyledItem{ .text = "", .style = fallback_style },
371+
} };
348372

349373
switch (val) {
350374
.string => |str| {
@@ -358,6 +382,11 @@ fn parseItem(val: std.json.Value, allocator: std.mem.Allocator, fallback_style:
358382
mode_aware_item.mode_aware.command = parseStyledItem(obj.get("command"), allocator, mode_aware_item.mode_aware.command);
359383
return mode_aware_item;
360384
}
385+
if (obj.contains("reload") or obj.contains("idle")) {
386+
reload_aware_item.reload_aware.idle = parseStyledItem(obj.get("idle"), allocator, reload_aware_item.reload_aware.idle);
387+
reload_aware_item.reload_aware.reload = parseStyledItem(obj.get("reload"), allocator, reload_aware_item.reload_aware.reload);
388+
return reload_aware_item;
389+
}
361390

362391
styled_item.styled = parseStyledItem(val, allocator, styled_item.styled);
363392
return styled_item;
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
const Self = @This();
2+
const std = @import("std");
3+
const vaxis = @import("vaxis");
4+
const Config = @import("../config/Config.zig");
5+
const Event = @import("../Context.zig").Event;
6+
7+
const Timer = std.time.Timer;
8+
9+
mutex: std.Thread.Mutex,
10+
condition: std.Thread.Condition,
11+
thread: ?std.Thread,
12+
should_quit: bool,
13+
loop: ?*vaxis.Loop(Event),
14+
reload_indicator_duration_ns: u64,
15+
reload_timer: ?Timer,
16+
generation: usize,
17+
pending: bool,
18+
config: *Config,
19+
20+
pub fn init(config: *Config) Self {
21+
return .{
22+
.mutex = std.Thread.Mutex{},
23+
.condition = std.Thread.Condition{},
24+
.thread = null,
25+
.should_quit = false,
26+
.loop = null,
27+
.reload_indicator_duration_ns = 0,
28+
.reload_timer = null,
29+
.generation = 0,
30+
.pending = false,
31+
.config = config,
32+
};
33+
}
34+
35+
pub fn deinit(self: *Self) void {
36+
self.mutex.lock();
37+
self.should_quit = true;
38+
self.condition.signal();
39+
self.mutex.unlock();
40+
41+
if (self.thread) |thread| {
42+
thread.join();
43+
self.thread = null;
44+
}
45+
}
46+
47+
pub fn start(self: *Self, loop: ?*vaxis.Loop(Event)) !void {
48+
self.mutex.lock();
49+
self.reload_indicator_duration_ns = @as(u64, @intFromFloat(@as(f32, self.config.file_monitor.reload_indicator_duration) * std.time.ns_per_s));
50+
self.loop = loop;
51+
self.reload_timer = try Timer.start();
52+
self.pending = false;
53+
self.mutex.unlock();
54+
55+
if (self.thread == null) {
56+
self.thread = try std.Thread.spawn(.{}, run, .{self});
57+
}
58+
}
59+
60+
fn run(self: *Self) void {
61+
const check_interval_ns = self.reload_indicator_duration_ns / 4;
62+
63+
while (true) {
64+
self.mutex.lock();
65+
66+
if (self.should_quit) {
67+
self.mutex.unlock();
68+
break;
69+
}
70+
71+
_ = self.condition.timedWait(&self.mutex, check_interval_ns) catch {};
72+
73+
const current_loop = self.loop;
74+
const generation = self.generation;
75+
const pending = self.pending;
76+
77+
var elapsed_ns: u64 = 0;
78+
if (self.reload_timer) |*timer| elapsed_ns = timer.read();
79+
80+
self.mutex.unlock();
81+
82+
if (pending and (elapsed_ns >= self.reload_indicator_duration_ns)) {
83+
if (current_loop) |loop| loop.postEvent(.{ .reload_done = generation });
84+
85+
self.mutex.lock();
86+
self.pending = false;
87+
if (self.reload_timer) |*timer| timer.reset();
88+
self.mutex.unlock();
89+
}
90+
}
91+
}
92+
93+
pub fn notifyChange(self: *Self) void {
94+
self.mutex.lock();
95+
self.generation += 1;
96+
if (self.reload_timer) |*timer| timer.reset();
97+
self.pending = true;
98+
self.condition.signal();
99+
self.mutex.unlock();
100+
}

0 commit comments

Comments
 (0)