Filter for Tasks #328
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Plan: filter the items of a state command by tags and by due range
Source: a handover spec and a clickable mock-up (
plan/README.md,plan/task-screen-mockup.html; the folder is not tracked, so everything you need is below). The mock-up's data and the dimension namesproject/areaare placeholders: use the realpartitioned_bydimensions of the command.Decisions (agreed with the user)
moment <= now. It is the default.(dimension, value). The app shows one chip row for allpartitioned_bydimensions, replacing today's one row per dimension.--due). They keep their partition flags as they are (AND across dimensions); only the app gets the OR selection.rememberSaveable, per command key, aspartitiondoes today.1. Core (
crates/streamd-core)commands/state/items.rspub enum Due { Now, Days(u32), Any }, which is how far into the future items are listed:Now:moment <= now(whatshow_future = falseis today).Days(n):moment < start_of_day(today + n + 1, tz), wheretodayis the date ofnowintz. Uselocalize::start_of_day, the one place a local day becomes a moment.Any: no limit (whatshow_future = trueis today).Due::shows(&self, moment, now, tz) -> bool, or resolve it into a limit before the loop.FromStr/Due::parsefor the CLI:now,any,<N>d(e.g.7d). An error names the accepted forms. AddCoreError::InvalidDue, or reuse an existing variant if one fits.Filtergets OR alternatives besides its AND partition:Filter::any_of(values: &[(String, String)], config)builds onequery::Conditionper(dimension, value)(alias-aware, likeCondition::new). An item passes if it matches any of them; an empty list passes everything.Filter::new(the AND map) stays for the CLI.Filter::shows(item, due, now, tz)replaces theshow_future: boolparameter. Updateshown_itemsthe same way.commands/state/board.rsboard(shards, command, filter, due, now, tz)instead ofshow_future.partition_countsis unchanged.api.rsstate_items(…, partition, due: Due, now): replacesshow_future. The tz comes fromproject.timezone().state_board(project, stream, key, any_of: &[(String, String)], due: Due, now): the partition becomes the OR list. Only the bindings call it;[]means all.partition_values(project, stream, key, due: Due, include_closed, now): counts overFilter::none()withindue, as today.Due::Any/Due::Now.Tests (core):
Days(7)includes the last minute of day 7 and excludes the first of day 8, also across a DST change (use a zone such asEurope/Berlin).Nowequals the oldshow_future = false,Anyequalstrue.any_ofwith two values of different dimensions shows the union. An alias inany_ofmatches its tag. Counts withinDays(7)include future items.2. Bindings (
crates/streamd-ffi)Due { Now, Days { days: u32 }, Any }torecords.rswith its conversion into the core type.project.stateItems(stream, key, partition, due, now),project.stateBoard(stream, key, anyOf: List<Placement>, due, now)andproject.partitionValues(stream, key, due, includeClosed, now)takeDueinstead ofshowFuture.tests/kotlin/StoreTest.kt(lines ~136, 155, 176, 270, 276) and add a Rust test thatDays(7)reaches a future item.3. CLI (
crates/streamd-cli)commands/state/cli.rs: add--due <now|Nd|any>, defaultnow, parsed byDue::parse(a wrong value is a usage error).--show-futurestays as a shorthand for--due anyand conflicts with--due.StateAction::{List, Interactive}carry aDueinstead ofshow_future. Add parse tests next to the existing ones.run.rs: pass theDuethrough.interactive/model.rs,view.rs,terminal.rs):fcyclesnow → 7d → any → now. Key bar: "change how far ahead items are shown". Summary:· due within 7 days/· with future items. Update the view tests.4. App (
android/)core/StateList.ktdata class PartitionTag(val dimension: String, val value: String).enum class DueRange { UntilNow, NextWeek, AnyDate }, mapped toDue.Now,Due.Days(7u),Due.AnyinStreamSession.data class TaskFilter(val due: DueRange = UntilNow, val tags: Set<PartitionTag> = emptySet()).StateLists.list(key, filter: TaskFilter, showClosed)replaces thepartitionmap.StateLists.preview(key, filter, showClosed): Previewed, withPreviewed.Done(matching: Int, partitions: List<Partition>)/Failed(reason). This is the live count of the sheet. It callsstateBoardandpartitionValuesonly and reads no file.matchingis the number of items in the listed states.core/StreamSession.ktlist:stateBoard(stream, key, filter.tags → Placements, due, now)andpartitionValues(…, due, showClosed, now).preview.Due.Nowwhere they passfalsetoday.Pure logic: new
ui/TaskFilter.kt, unit-tested insrc/test/topFive(partitions): every(dimension, value)of all partitions, sorted by count descending, then value, then dimension; the first five.isDefault(filter, top):due == UntilNowand (no tags, or exactly one tag that is intop). This drives the badge dot.chipRow(filter, top): the chips in order: the scope chip ifdue != UntilNow,All, a selected chip for each selected tag outsidetop(sorted like the list), then the top five.tapped(filter, tag?): a chip replaces the selection with its tag;All(null) clears it; the due range is kept.tagRows(partitions, query): all values sorted astopFivesorts them, each with whether it is top five and its count. Matching is case-insensitive on the value or on the dimension name, ignoring a leading@in the query. It returns the matched range in the value and/or in the dimension, for the highlight.tagsSummary(n): "all tags" / "N selected".reset():TaskFilter().dueHint(moment, now, zone)for a future item: "today" (later today), "tomorrow", "in N days" (plurals resource). Null if the moment is not after now.keeping(StateMoves.kt) becomesTaskFilter.keeping(partitions): it drops the selected tags that are no longer among the values.ui/StateList.ktStateListScreen: keepsfilter: TaskFilterinrememberSaveable(key)with aSaver(the due range by name, the tags as pairs of strings) instead ofpartition, andsheetOpen. TheLaunchedEffectkeys onfilter.Header: anIconButtonwithStreamdIconsforic_tune.xml(it is already inres/drawable; add it toIcons.ktif missing), between Search and ⋮, with content description "Filter tasks" (string resource). Wrap it inBadgedBox { Badge() }while!isDefault.PartitionChips: one horizontally scrolling row fromchipRow. Tag chips are labelledvalue count, as today. The scope chip ("Next 7 days" / "Any date" withic_expand_more) is aFilterChip/AssistChipoutlined and labelled incolorScheme.primary; it opens the sheet.ItemRow: whendueHintis not null, the date part of the supporting line is drawn incolorScheme.primaryfollowed by· in 8 days, e.g.2026-11-02 · in 8 days · <file title>. No literal colours: use theme roles.Filter sheet: new
ui/TaskFilterSheet.ktTaskFilterSheetwraps aModalBottomSheet(drag handle,skipPartiallyExpanded = true) aroundinternal fun TaskFilterSheetContent(draft, preview: Previewed?, query, onDraft, onQuery, onReset, onApply). This split is for the screenshot tests: layoutlib does not render the window of a modal sheet, so the content is drawn directly.rememberof the applied filter when it opens) and askslists.previewwhenever the draft changes. Dismissing it (scrim, swipe, back) discards the draft. "Show N tasks" applies it and closes the sheet.TextButton"Reset" on the right. It setsTaskFilter()and is disabled when the draft already equals it.ConnectedChoice(Components.kt) with "Until today" / "Next 7 days" / "Any date". One line of helper text below:tagsSummaryon the right, thenSearchField(hint "Search tags or dimensions", a clear (x) button while text is entered). The rows are in aLazyColumn; each row has:Checkbox(multi-select),@ValueinStreamdTheme.monowith the tint of the tag (TagTint/tagSpan, as tags are coloured elsewhere) and the match highlighted (e.g. bold or a background on the matched span),onSurfaceVariantmono, also highlighted,With no match it shows
No tag matches "<query>".The counts come from the preview, so they follow the draft's due range.Button"Show N tasks" (plurals), or "No tasks match" when 0. It stays enabled at 0, so an empty result can still be applied, as in the mock-up.res/values/strings.xml, counts as<plurals>.Screenshot tests (
StateListScreenshots.kt)StateListFilteredScreenshot: Next 7 days, the scope chip, a selected non-top-five chip, the badge, future rows with their hint.StateListFilterSheetScreenshot: the default draft, with more than five tags over two dimensions.StateListFilterSheetSearchScreenshot: a query matching a dimension name, highlighted.StateListFilterSheetNoMatchScreenshot.StateListFilterSheetEmptyScreenshot: "No tasks match".SurfacewithinScreen { }, with a fixedNOW(Fixtures.kt).updateDebugScreenshotTestand look at every image.TaskFilter.kt, and for theSaver.5. Documentation
--due,--show-future);fkey of the interactive view (line ~611 on).--dueforms,f);Dueof the bindings:stateItems,stateBoardwith OR placements,partitionValues);commands/state/(Due,Filter::any_of);state_boardtakes OR values and aDue,partition_valuesaDue);records.rs(Due);StreamSession(list(key, filter, showClosed),preview);ui/(TaskFilter.kt,TaskFilterSheet.kt).Acceptance
@; the match is highlighted; there is a clear button and the no-match line.streamd task --due 7d,--due any,--show-future;fcycles instreamd task -i.nix flake checkpasses;nix build .#streamd-apppasses (unit and screenshot tests).Implemented in #331.
Time: about 25 minutes, including
nix flake check, the bindings build andnix build .#streamd-app.Tokens: about 255k.
Findings during the implementation
--dueis a new flag on every state command. Validation reserved onlyshow-futureandhelpas flag names, sodueis now reserved too: apartitioned_bydimension namedduewould have clashed with it.Days(n)is resolved as "the start of day n+1 in the project's timezone, minus one nanosecond", throughstart_of_day. That makes it exact across a clock change; a test on the Berlin change of 2026-10-25 covers it.Due::parseuses its own smallDueErrorwithFromStrinstead of a newCoreErrorvariant. Only the CLI parses it; the bindings pass the enum directly.fnow cycles three ranges (now → 7d → any). A front-end test that expected one press to show everything was adjusted.keeping(StateMoves.kt) becameTaskFilter.keeping; chosen values that disappear are still cleared.rememberModalBottomSheetStateis deprecated in the pinned Material version. It is kept, likeActivityRange.kt, so both sheets can move to the new API together.strings.xml.