╭──────────╮ │← previous│ ╰──────────╯ ╭ changed ───────────────╮ │src/ui/widget/thread.zig│ ╰────────────────────────╯ ╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │833 var tag_iter = Event.tagIterator(entry.record.event.tags); │ │834 while (tag_iter.next()) |tag| { │ │835 if (tag.len == 0) continue; │ │646 - try items.append(allocator, .{ .text = tag, .link = try tagLink(self.session.page_arena, self.data.identity, entryStatus(entry), tag) }); │ │836 + try items.append(allocator, .{ .text = tag, .link = try tagLink(self.session.page_arena, self.data.identity, entryStatus(entry), tag, self.data.search orelse "") });│ │837 } │ │838 if (items.items.len > 0) { │ │839 var tags = try TagFlow.init(allocator); │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1516 return if (supports_conflicts) Data.resolveRoute(identity, selected, picks) else null; │ │1517 } │ │1518 │ │1329 - fn listLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, status: Status, tag: []const u8, id: []const u8) ![]const u8 { │ │1330 - const route = listRoute(identity, status, tag, id) orelse return error.RouteTooLong; │ │1519 + // a list link keeps the filters the page carries: the tag, and the │ │1520 + // search query when one is set │ │1521 + fn listLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, status: Status, tag: []const u8, id: []const u8, search: []const u8) ![]const u8 {│ │1522 + var route = listRoute(identity, status, tag, id) orelse return error.RouteTooLong; │ │1523 + if (search.len != 0) route = route.withSearch(search) orelse return error.RouteTooLong; │ │1524 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{try route.toUrl(page_arena)}); │ │1525 } │ │1526 │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1547 const route = draftsRoute(data.identity) orelse return error.RouteTooLong; │ │1548 return std.fmt.allocPrint(page_arena.allocator(), "a:{s}", .{try route.toUrl(page_arena)}); │ │1549 } │ │1357 - return listLink(page_arena, data.identity, status_maybe orelse @enumFromInt(0), data.tag, id); │ │1550 + return listLink(page_arena, data.identity, status_maybe orelse @enumFromInt(0), data.tag, id, data.search orelse ""); │ │1551 } │ │1552 │ │1360 - fn tagLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, status: Status, tag: []const u8) ![]const u8 { │ │1553 + fn tagLink(page_arena: *std.heap.ArenaAllocator, identity: []const u8, status: Status, tag: []const u8, search: []const u8) ![]const u8 {│ │1554 const encoded = try ui.urlEncodeRef(page_arena.allocator(), tag); │ │1362 - return listLink(page_arena, identity, status, encoded, ""); │ │1555 + return listLink(page_arena, identity, status, encoded, "", search); │ │1556 } │ │1557 │ │1558 pub fn init(allocator: std.mem.Allocator, data: *const Self, session: *ui.Session) !This { │ ╰───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │1589 defer items.deinit(allocator); │ │1590 // when filtered, the first item clears the filter │ │1591 if (data.tag.len != 0) │ │1399 - try items.append(allocator, .{ .text = "✕", .link = try listLink(session.page_arena, data.identity, @enumFromInt(0), "", "") }); │ │1592 + try items.append(allocator, .{ .text = "✕", .link = try listLink(session.page_arena, data.identity, @enumFromInt(0), "", "", data.search orelse "") });│ │1593 for (data.tags) |tag| │ │1401 - try items.append(allocator, .{ .text = tag, .link = try tagLink(session.page_arena, data.identity, @enumFromInt(0), tag) }); │ │1594 + try items.append(allocator, .{ .text = tag, .link = try tagLink(session.page_arena, data.identity, @enumFromInt(0), tag, data.search orelse "") }); │ │1595 try tf.setItems(allocator, items.items); │ │1596 try stack.children.put(allocator, tf.getFocus().id, .{ .tag_flow = tf }); │ │1597 } │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2355 raw_key; │ │2356 │ │2357 if (self.headerActive()) { │ │2165 - // down from the tabs re-enters the stack if the selected view has │ │2166 - // something to focus; other keys move the tabs. │ │2358 + // down from the sub-header re-enters the stack if the selected │ │2359 + // view has something to focus; enter in the search box submits │ │2360 + // the query; other keys move within the sub-header. │ │2361 if (inp.vertDirection(key) == .down) { │ │2362 const enterable = if (self.viewStack().getSelected()) |selected| switch (selected.*) {│ │2363 .tag_flow => |*tf| tf.text_boxes.items.len > 0, │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2366 else => true, │ │2367 } else false; │ │2368 if (enterable) root_focus.setFocus(self.box.children.keys()[stack_index]);│ │2369 + } else if (key == .enter) { │ │2370 + if (try self.header().submittedText(allocator)) |text| { │ │2371 + defer allocator.free(text); │ │2372 + try self.submitSearch(text); │ │2373 + } │ │2374 } else { │ │2375 try self.header().input(allocator, key, root_focus); │ │2376 } │ ╰────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮ │2402 } │ │2403 } │ │2404 │ │2405 + // navigate to the typed query's results, keeping the tag filter and the │ │2406 + // status list the page shows. an empty box clears a filtered page and │ │2407 + // does nothing on any other one. │ │2408 + fn submitSearch(self: *This, text: []const u8) !void { │ │2409 + if (text.len == 0 and self.data.search == null) return; │ │2410 + const selected = self.header().getSelectedIndex() orelse 0; │ │2411 + const status: Status = if (selected < status_count) splitStatus(selected) else @enumFromInt(0);│ │2412 + const route = listRoute(self.data.identity, status, self.data.tag, "") orelse return; │ │2413 + try self.session.navigate(route.withSearch(text) orelse return); │ │2414 + } │ │2415 + │ │2416 // arrow keys move the tag selection; up from the top row crosses to the │ │2417 // header tabs. │ │2418 fn tagsViewInput(self: *This, key: Key, root_focus: *Focus) void { │ ╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯ ╭ added ──────────────╮ │src/search_thread.zig│ ╰─────────────────────╯