Class: OKF::TUI::Workspace

Inherits:
Object
  • Object
show all
Defined in:
lib/okf/tui/workspace.rb

Overview

Every bundle the session can see, which one is active, and which ones a search covers.

Two ways in, mirroring what okf server accepts: named directories are ad-hoc and never touch the registry, and with no directories the registry itself is the workspace. Registering stays an explicit act — an ad-hoc look at two bundles should not enrol them in the user's durable list.

Defined Under Namespace

Classes: Entry, Group

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(dirs: [], ref_slugs: {}, home: nil, cwd: nil) ⇒ Workspace

cwd is what opts this workspace into registry discovery, and it is deliberately not defaulted to Dir.pwd: okf draws the same line, where only its CLI passes a cwd and a library caller stays on the global registry. A default here would make the registry an embedding app reads depend on the directory its process happens to be in, and would let the suite discover a .okf-registry.json from wherever rake was run.

ref_slugs maps a resolved directory to the slug the @ref named it by, so a session built from refs keeps the names the user typed. See Refs#slugs.



88
89
90
91
92
93
94
# File 'lib/okf/tui/workspace.rb', line 88

def initialize(dirs: [], ref_slugs: {}, home: nil, cwd: nil)
  @home = home
  @cwd = cwd
  @dirs = Array(dirs)
  @ref_slugs = ref_slugs || {}
  load_entries
end

Instance Attribute Details

#active_slugObject (readonly)

Returns the value of attribute active_slug.



77
78
79
# File 'lib/okf/tui/workspace.rb', line 77

def active_slug
  @active_slug
end

#entriesObject (readonly)

Returns the value of attribute entries.



77
78
79
# File 'lib/okf/tui/workspace.rb', line 77

def entries
  @entries
end

#homeObject (readonly)

Returns the value of attribute home.



77
78
79
# File 'lib/okf/tui/workspace.rb', line 77

def home
  @home
end

#registryObject (readonly)

Returns the value of attribute registry.



77
78
79
# File 'lib/okf/tui/workspace.rb', line 77

def registry
  @registry
end

#search_errorObject (readonly)

Set when a query could not be answered and the reason is worth showing. nil after any search that ran, so it never outlives the query it describes.



244
245
246
# File 'lib/okf/tui/workspace.rb', line 244

def search_error
  @search_error
end

Instance Method Details

#activeObject



133
134
135
# File 'lib/okf/tui/workspace.rb', line 133

def active
  entry(@active_slug)
end

#add(dir) ⇒ Object

── config: every registry write lands here ─────────────────────────────

Each returns a message for the status line and reloads, so what the screen shows next is what the file now says rather than what the in-memory list was talked into believing.



252
253
254
255
256
257
258
259
260
# File 'lib/okf/tui/workspace.rb', line 252

def add(dir)
  return not_registry_backed unless registry_backed?

  added = registry.add(File.expand_path(dir.to_s.strip))
  reload
  "registered @#{added.slug}#{added.path}"
rescue OKF::Error => e
  "could not add: #{e.message}"
end

#add_to_group(slug, members) ⇒ Object



322
323
324
325
326
327
328
329
330
331
332
333
334
335
# File 'lib/okf/tui/workspace.rb', line 322

def add_to_group(slug, members)
  return not_registry_backed unless registry_backed?
  return "nothing in scope to add" if members.empty?

  registry.set_group(slug, refs(members))
  reload
  # One member named, several counted: `+` adds the row under a cursor and the
  # line has to say *which* row landed, while `c` adds a whole scope and naming
  # every slug would be a list, not a message.
  total = count(group(slug)&.size.to_i)
  members.length == 1 ? "@#{members.first} joined @#{slug}#{total}" : "@#{slug} now names #{total}"
rescue OKF::Error => e
  "could not add to @#{slug}: #{e.message}"
end

#create_group(slug, members) ⇒ Object

── groups: the writes okf's registry group / ungroup make ──────────

A group is registry configuration in exactly the sense a slug rename is, so it belongs on the same side of the line as the writes above — see [registry-write-boundary]. Members come from the scope rather than from a typed list: already means "these bundles" in this view, so the gesture is toggle what you want, then name it.

okf owns the cascades. set_group creates-or-adds and refuses a cycle, unset_group_members deletes a group it empties, and rename/remove span a group slug and cascade through every member list — none of which is reimplemented here.



308
309
310
311
312
313
314
315
316
317
318
319
320
# File 'lib/okf/tui/workspace.rb', line 308

def create_group(slug, members)
  return not_registry_backed unless registry_backed?

  slug = slug.to_s.strip
  return "group cancelled: no name given" if slug.empty?
  return "group cancelled: no bundles in scope to name" if members.empty?

  group = registry.set_group(slug, refs(members))
  reload
  "@#{group.slug} names #{count(members.length)}"
rescue OKF::Error => e
  "could not create the group: #{e.message}"
end

#empty?Boolean

Returns:

  • (Boolean)


111
112
113
# File 'lib/okf/tui/workspace.rb', line 111

def empty?
  entries.empty?
end

#entry(slug) ⇒ Object



115
116
117
# File 'lib/okf/tui/workspace.rb', line 115

def entry(slug)
  entries.find { |candidate| candidate.slug == slug }
end

#group(slug) ⇒ Object



129
130
131
# File 'lib/okf/tui/workspace.rb', line 129

def group(slug)
  groups.find { |candidate| candidate.slug == slug }
end

#groupsObject

The registry's groups. Empty for an ad-hoc workspace, which has no registry to have groups in.

Read from okf rather than derived: groups_listing is what okf registry list prints and expand is what okf search @group resolves, so a group means the same set here as it does at the command line.



125
126
127
# File 'lib/okf/tui/workspace.rb', line 125

def groups
  @groups ||= registry_backed? ? registry_groups : []
end

#make_default(slug) ⇒ Object



272
273
274
275
276
277
278
279
280
# File 'lib/okf/tui/workspace.rb', line 272

def make_default(slug)
  return not_registry_backed unless registry_backed?

  registry.default = slug
  reload
  "@#{slug} is now the default"
rescue OKF::Error => e
  "could not set default: #{e.message}"
end

#modelObject

The active bundle's Model, or nil when nothing is loadable. Every single-bundle view reads through this.



139
140
141
# File 'lib/okf/tui/workspace.rb', line 139

def model
  active&.model
end

#registry_backed?Boolean

A registry-backed workspace is the one that can be configured; a workspace of named directories has no registry to write to.

Returns:

  • (Boolean)


98
99
100
# File 'lib/okf/tui/workspace.rb', line 98

def registry_backed?
  @dirs.empty?
end

#registry_pathObject

The registry file this session is on — the discovered project-local one when there is one, else the global $OKF_HOME one. Asked of the registry rather than recomputed from home, because Registry.path only ever names the global file and would report the wrong one out of a project directory: the header would print a path the session is not reading.



107
108
109
# File 'lib/okf/tui/workspace.rb', line 107

def registry_path
  (registry || open_registry).path
end

#reloadObject

Re-read from disk, keeping the active bundle and the scope wherever they still resolve.



355
356
357
358
359
360
361
362
363
364
365
# File 'lib/okf/tui/workspace.rb', line 355

def reload
  previous_active = @active_slug
  previous_scope = @scope

  load_entries

  @active_slug = previous_active if entry(previous_active)&.loaded?
  @active_slug ||= first_loaded_slug
  kept = previous_scope.select { |slug| entry(slug) }
  @scope = kept.empty? ? entries.map(&:slug) : kept
end

#remove(slug) ⇒ Object



262
263
264
265
266
267
268
269
270
# File 'lib/okf/tui/workspace.rb', line 262

def remove(slug)
  return not_registry_backed unless registry_backed?

  registry.remove(slug)
  reload
  "removed @#{slug} from the registry (the bundle itself is untouched)"
rescue OKF::Error => e
  "could not remove: #{e.message}"
end

#remove_from_group(slug, members) ⇒ Object



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/okf/tui/workspace.rb', line 337

def remove_from_group(slug, members)
  return not_registry_backed unless registry_backed?
  return "nothing in scope to remove" if members.empty?

  registry.unset_group_members(slug, refs(members))
  reload
  # okf deletes a group its last member left, so say so rather than leave the
  # user looking for a row that is gone.
  return "@#{slug} had nothing left, so it is gone" if group(slug).nil?

  total = count(group(slug).size)
  members.length == 1 ? "@#{members.first} left @#{slug}#{total}" : "@#{slug} now names #{total}"
rescue OKF::Error => e
  "could not remove from @#{slug}: #{e.message}"
end

#rename(old_slug, new_slug) ⇒ Object



282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/okf/tui/workspace.rb', line 282

def rename(old_slug, new_slug)
  return not_registry_backed unless registry_backed?

  new_slug = new_slug.to_s.strip
  return "rename cancelled: no name given" if new_slug.empty?

  renamed = registry.rename(old_slug, new_slug)
  reload
  "@#{old_slug} is now @#{renamed.slug}"
rescue OKF::Error => e
  "could not rename: #{e.message}"
end

#scopeObject

Kept in workspace order and filtered to slugs that still exist, so a reload after a remove cannot leave a stale slug in the scope.



154
155
156
# File 'lib/okf/tui/workspace.rb', line 154

def scope
  entries.map(&:slug).select { |slug| @scope.include?(slug) }
end

#scope_allObject



166
167
168
# File 'lib/okf/tui/workspace.rb', line 166

def scope_all
  @scope = entries.map(&:slug)
end

#scope_group(slug) ⇒ Object

Scope the search to a group — the reason a group is worth showing here at all. okf search @mkt merges exactly these bundles into one ranking; this is the same set, named the same way, reached with one key instead of retyping the members.

Returns a message for the status line, like the config writes do, because each of the three ways this can decline to do anything needs saying: a scope that silently did not change reads as a broken key.



186
187
188
189
190
191
192
193
194
195
196
# File 'lib/okf/tui/workspace.rb', line 186

def scope_group(slug)
  group = group(slug)
  return "no such group: @#{slug}" if group.nil?
  return "@#{slug} names a cycle in #{registry.path} — okf cannot resolve it" if group.cyclic?
  return "@#{slug} resolves to no bundle here" if group.bundles.empty?

  # A copy: `toggle_scope` mutates the scope in place, and the group is a
  # description of the registry, not a scratch list.
  @scope = group.bundles.dup
  "search scope: @#{slug}#{group.size} #{group.size == 1 ? "bundle" : "bundles"}"
end

#scope_noneObject



170
171
172
# File 'lib/okf/tui/workspace.rb', line 170

def scope_none
  @scope = []
end

#scope_only(slug) ⇒ Object



174
175
176
# File 'lib/okf/tui/workspace.rb', line 174

def scope_only(slug)
  @scope = [ slug ]
end

#scoped?(slug) ⇒ Boolean

Returns:

  • (Boolean)


158
159
160
# File 'lib/okf/tui/workspace.rb', line 158

def scoped?(slug)
  @scope.include?(slug)
end

#search(query, mode: :fuzzy) ⇒ Object

Ranked search across every scoped bundle, merged into one ordered list. They share one index on purpose: BM25 weighs a term by how rare it is in the corpus, so per-bundle indexes would score the same match differently depending on which bundle it came from. One index makes one corpus — the same thing okf search @all does.

The mode is what chooses okf's engine, by declaring the capability the query needs rather than by naming an engine:

:fuzzy   `fuzzy: true`  → the full-text index. Ranked BM25, typo-tolerant.
:text    nothing        → the scan, okf's own default. Raw substring.
:regexp  `regexp: true` → the scan, as a pattern.

All three are reachable because the index and the scan disagree by design, and each is wrong for what the other is right for. okf documents the index's limits precisely: its tokenizer splits on punctuation, so 7.2.0 indexes as 7, 2, 0, and a backtick is not punctuation, so a word inside a code span indexes as `minifts` and the query minifts does not match it. Measured on okf's own bundle, the index finds three of the five concepts that say minifts, and returns fourteen for OKF_HOME where the scan returns five. Offering only the index left the terms glued to symbols — a constant, an env var, a version — unfindable, with nothing on screen saying so.



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/okf/tui/workspace.rb', line 220

def search(query, mode: :fuzzy)
  @search_error = nil
  terms = query.to_s.split(/\s+/).reject(&:empty?)
  return [] if terms.empty?

  corpus = search_corpus
  return [] if corpus.nil?

  run_search(corpus, terms, mode)
rescue RegexpError => e
  # A bad pattern is the user's typo, not a broken install, and it must not
  # land in the blanket rescue below — "no matches" for an unparseable regexp
  # is the silent-wrong-answer shape this view keeps having to avoid.
  @search_error = "bad pattern: #{e.message}"
  []
rescue OKF::Bundle::Search::UnsupportedQuery => e
  @search_error = e.message
  []
rescue StandardError
  []
end

#switch(slug) ⇒ Object



143
144
145
146
147
148
# File 'lib/okf/tui/workspace.rb', line 143

def switch(slug)
  return false unless entry(slug)&.loaded?

  @active_slug = slug
  true
end

#toggle_scope(slug) ⇒ Object



162
163
164
# File 'lib/okf/tui/workspace.rb', line 162

def toggle_scope(slug)
  @scope.include?(slug) ? @scope.delete(slug) : @scope << slug
end