hiiro 0.1.390 → 0.1.392

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e9c16c581ea6c758abd518a2f706d980cd7e7d4f2c0efad6fb8f99de4ba8fcdb
4
- data.tar.gz: 85fbe331a3e3ddf3fd36adfb3864d5f4d131a17714be759a65eaf79940f423ce
3
+ metadata.gz: ad2d60d5740dd8e55e05cb747b2bd6b5e29a04e0afea78044d27b8bd07d89d4b
4
+ data.tar.gz: edbb5719e7e2e7fca15751c62140f1c1ef8eb25f62d52a8d3e4d9369e86d400c
5
5
  SHA512:
6
- metadata.gz: ab5e23b6ebe2c5e0fbfb17000ce471c965efc3988777e346d15bbaeb54d1bc3ca1479f0ed21c98a52a0f959d1066f6c0c936a665e71ba29d3132c6f64c70062d
7
- data.tar.gz: eb6a4353ae61c553c5f1cf524e18548161ff201a465cdf682ea04e897028b6b7df56ce356fd57259ddc7fd3951370b8ad65691453bbf6995c25a68869af2159b
6
+ metadata.gz: 3b77ec5fd8c0035039f265df261e982d7e02d730292fdf91196322cce5dba1ce66683e058cbd5816c02d1dc9f5cb1392e32ff8a4638f3766413b8ed28f57db75
7
+ data.tar.gz: 1dfc40943e3a3dcd494eae3f0f9ca642b4cbf926db55a5a64ff256003a1fc7b2bb556677c47cdb1b0a2058b8fab42c836424e6b363cf586cae76e6b8190805f5
data/AGENTS.md CHANGED
@@ -24,6 +24,18 @@ Tests are organized under `test/`:
24
24
 
25
25
  The `Hiiro::TestHarness` class (in `test/test_helper.rb`) enables testing bin files by capturing the block passed to `Hiiro.run` and evaluating it in a test context with stubbed `system` calls.
26
26
 
27
+
28
+ ## Making Changes
29
+
30
+ Hiiro is extensible, and when working on a bin or subcommand, there could be
31
+ multiple variations of a bun and which one the user assumes is latest might not
32
+ be accurate. The common places for these bins are:
33
+
34
+ - ~/bin/
35
+ - ~/proj/phome/bin/
36
+ - ~/proj/hiiro/bin/
37
+ - any version of the gem installed via rbenv
38
+
27
39
  ## Architecture
28
40
 
29
41
  ### Core Components (bin/h)
@@ -239,6 +251,59 @@ end
239
251
  - `h db migrate` — re-run YAML import (if not yet migrated)
240
252
  - `h db restore` — restore YAML files from SQLite data
241
253
 
254
+ ### Hiiro::Table / Hiiro::TableScope / Hiiro::TableActions (lib/hiiro/table*.rb)
255
+
256
+ A **table** is one named key/value list, stored as `registry.resource_type`; an **entry** is one `RegistryEntry` row. `RegistryEntry` has a nullable `task_id` and `tags` / `tag(*names)` / `untag(*names)` through `Hiiro::Tag` (taggable_type `RegistryEntry`).
257
+
258
+ ```ruby
259
+ Hiiro::Table.all # every table, sorted by name
260
+ t = Hiiro::Table.named('link') # exists? is false until a row is added
261
+ t.add('gh', value: 'https://github.com', short: 'g', desc: '', meta: {}, task_id: nil)
262
+ t.entries(scope: Hiiro::TableScope.global) # rows ordered by name; count(scope:) ; find(ref)
263
+ t.rename('links') # moves every row; raises Hiiro::Error if the target exists
264
+
265
+ scope = Hiiro::TableScope.current # task scope when CurrentTask resolves one, else global
266
+ scope.task? / scope.global? / scope.label # "task(@NAME)" | "global"
267
+ scope.toggle # task -> global keeps the task; global -> task re-uses or resolves it
268
+ scope.apply(dataset) # the only place that knows task scope means where(task_id:)
269
+
270
+ acts = Hiiro::TableActions.load # ~/.config/hiiro/tables.yml: { 'link' => { 'c' => 'cd {value}' } }
271
+ acts.for(entry) # DEFAULTS (o open, y pbcopy) merged with the table's letters
272
+ acts.command('o', entry) # {value} {name} {table} shell-escaped; nil when no action
273
+ acts.run('o', entry) # system('/bin/sh', '-c', cmd); returns cmd
274
+ ```
275
+
276
+ `bin/h-table` is the TUI (`Hiiro::TableScreen` in lib/hiiro/table_screen.rb: `TableScreen.new(scope:, table:, actions:)`, `reload!`, `marked`) and the scriptable CLI (`tables`, `ls`, `add`, `rm`, `mv`, `set`, `get`, `tag`, `untag`) over these tables; `h registry` is the same rows. See `docs/h-table.md`.
277
+
278
+ `t ui` is the second screen on the toolkit: `Hiiro::TaskScreen` (lib/hiiro/task_screen.rb, `TaskScreen.new(scope: :global, current:, show_closed: false, ops:)`, `reload!`, `expanded`) is a tree of task/subtask/todo nodes; edits go through `TaskRecord#set_next!` / `#set_waiting!` / `#set_status!`, `TodoItem#update_status`, and `TaskScreen::Ops` (create, save_current, open_workspace over `TaskCli::Commands`, stdout captured). See `docs/t.md`.
279
+
280
+ `h project ui` is the third screen on the toolkit: `Hiiro::ProjectCatalog.load(roots:, aliases:)` (lib/hiiro/project_catalog.rb; `default_roots` is `~/proj` and `WORK_DIR`, `rows` / `repos` / `reload` / `repo_for(row)`; a `Row` is `{ name, path, kind: :tree | :dir | :alias, repo, branch, head }`, `key` is the path, `main?` is the repo's first worktree) lists one row per git worktree under the roots plus plain dirs and `project` aliases, grouping a repo's worktrees by `Git::Worktrees.fetch` so an extra tree reads `repo ▸ tree`. `Hiiro::ProjectScreen.new(catalog:, scope:, current:, facts:, opener:, fetcher:)` (lib/hiiro/project_screen.rb, `reload!`, `rebuild!`) draws it; `Facts` caches `status[tree_path]` / `branches[repo_path]` / `prs[repo_path]` fetched on `s` / `b` / `p` through `Fetcher` (git and `gh pr list`), `Opener#open(row)` runs `tuios new-window` under `TUIOS_ENV=1` (`command(row)` is the argv) and `Herdr.open_workspace` otherwise, and `a` / `d` add and remove worktrees through git. See `docs/h-project.md`.
281
+
282
+ ### Hiiro::Tui (lib/hiiro/tui.rb)
283
+
284
+ Screen toolkit for in-process TUIs. One engine for every hiiro TUI; do not add curses or shell out to a fuzzy finder for a screen (see `docs/plans/hiiro-tui.md`).
285
+
286
+ ```ruby
287
+ class MyScreen < Hiiro::Tui::Screen
288
+ key 'o', :open_item # key map is a table; subclasses add or override bindings
289
+ def header_lines = ["title #{state.query}"]
290
+ def detail_lines(item) = ["name #{item.name}"] # nil (default) means no detail pane
291
+ def pick_candidates = %w[link sound] # '@' opens a prefix-filtered picker in the status line
292
+ def picked(candidate) = (self.message = candidate)
293
+ def open_item = (self.message = "open #{current_item}")
294
+ def unhandled_key(key) = nil # list-mode keys the key map does not bind
295
+ def ask = prompt('name', default: 'x') { |text| prompt('value') { |v| save(text, v) } }
296
+ end
297
+ MyScreen.new(items: rows).run # j/k/h/l move, '/' searches, '@' picks, q/Esc/Ctrl-C quit
298
+ ```
299
+
300
+ - `state` is one `Tui::State` (`mode` :list|:search|:pick|:prompt, `cursor`, `top`, `horizontal_offset`, `query`, `pick_query`, `message`, `prompt_label`, `prompt_text`). Read it; never keep a parallel boolean.
301
+ - `prompt(label, default: '') { |text| ... }` reads one line in the status line (`label: text_`). Enter leaves the mode and calls the block, Esc leaves without calling it, Ctrl-C quits. The block runs after the prompt closes, so it may call `prompt` again to chain questions.
302
+ - `unhandled_key(key)` is called from list mode for any key the key map does not bind (default no-op); `TableScreen` uses it for per-table action letters.
303
+ - `frame(rows:, cols:)` returns plain strings with no ANSI, one per row, which is what tests assert against literally. `render` adds the highlight.
304
+ - `Tui::Pane.new(width:, height:).lines(texts)` pads and truncates to an exact rectangle.
305
+ - `ListScreen` is `Screen` without a detail pane and keeps the `Showing a-b of n` footer.
306
+
242
307
  ### Hiiro::Fuzzyfind (lib/hiiro/fuzzyfind.rb)
243
308
 
244
309
  Integration with `sk` (skim) or `fzf` fuzzy finders:
@@ -371,9 +436,9 @@ loc.target # pane_id || tab_id || workspace_i
371
436
  loc.to_meta # inverse of from_meta (queue/service metadata)
372
437
  ```
373
438
 
374
- ### Hiiro::Link / Hiiro::URLTemplate (lib/hiiro/link.rb)
439
+ ### Hiiro::URLTemplate (lib/hiiro/link.rb)
375
440
 
376
- `bin/h-link` uses `Hiiro::Link` directly (its inline `LinkManager::Link` copy is gone). `Link.from_hash(yaml_row)` builds an unsaved instance; `to_h` / `to_yaml` give the links.yml shape.
441
+ Saved links are the `link` registry table (url → value, shorthand → short_name, description, created_at for the numbered order); `bin/h-link` reads and writes them through `Hiiro::Table.named('link')` and wraps each `RegistryEntry` in its own `LinkPresenter`. `h link path` prints the database path. `DB.import_tables!` imported the legacy `links` table (kept for the installed gem) and `links.yml` (renamed `.bak`) once; `Hiiro::DB.link_name(url, shorthand, description)` picks the entry name.
377
442
 
378
443
  ```ruby
379
444
  t = Hiiro::URLTemplate.new('https://x/{org}/{repo}', values: { 'org' => 'a b' })
@@ -660,16 +725,17 @@ ALWAYS update `README.md` and any files in `docs/` or other markdown files that
660
725
 
661
726
  All config lives in `~/.config/hiiro/`:
662
727
  - `plugins/` - Auto-loaded plugin files
663
- - `pins/` - Per-command YAML key-value storage
728
+ - `pins/` - Legacy per-command pin files. Pins now live in registry tables named after the command (e.g. table `t`, entry `current_task`); `pins/*` files and `sounds.yml` are imported once into tables `<name>` and `sound` by `Hiiro::DB.import_tables!` and renamed `.bak`
664
729
  - `tasks/` - Task metadata for worktree management
665
730
  - `queue/` - Prompt queue (wip, pending, running, done, failed)
666
731
  - `services/` - Service runtime state
667
732
  - `jumplist/` - Per-client tmux navigation history
668
733
  - `env_templates/` - Base .env template files for services
669
- - `projects.yml` - Project directory aliases
734
+ - `projects.yml` - Legacy project aliases; imported once into registry table `project` and renamed `.bak`. Aliases now live in that table; `h project edit` edits it through a YAML buffer
670
735
  - `apps.yml` - App directory mappings for task plugin
671
736
  - `services.yml` - Service and service group definitions
672
737
  - `tools.yml` - Runner tool definitions
738
+ - `tables.yml` - Per-table action letters for `h table`
673
739
  - `app_files.yml` - Per-app tracked file lists
674
740
 
675
741
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.392] - 2026-10-12
4
+
5
+ ### Changed
6
+ - `add_cmd` accepts `opts`, `flags`, and `mopts` arrays for value options, boolean flags, and repeated value options. Existing definitions are reused, and new short names use the first available alphanumeric character in the full name.
7
+ - Automatic command definitions are registered for reuse by later commands. Existing boolean callers now use `flags`.
8
+
3
9
  ## [0.1.390] - 2026-10-07
4
10
 
5
11
  ### Added
@@ -18,6 +24,9 @@
18
24
 
19
25
  ## Unreleased
20
26
 
27
+ ### Fixed
28
+ - Fix `PsProcess::Snapshot.capture` constant lookup, which raised `NameError` in `h ps byport`, `h ps indir`, and parent/child process lookups.
29
+
21
30
  ### Added
22
31
  - `h herdr` and `hh` pane controls for Vim-style focus, two-pane reflow, resize, swap, and zoom
23
32
  - `prefix+shift+h/j/k/l` reflows a two-pane tab toward an edge; `prefix+alt+h/j/k/l` resizes the current pane
data/README.md CHANGED
@@ -59,6 +59,13 @@ h ping
59
59
  # => pong
60
60
  ```
61
61
 
62
+ ### Find processes by port
63
+
64
+ `h ps byport 4399` prints each matching process PID and working directory,
65
+ separated by a tab. Pass multiple ports with `h ps byport 4399 3000`.
66
+ If no processes match, it prints `No processes found on port(s): 4399`.
67
+ This command requires `ps` and `lsof`.
68
+
62
69
  ### Save clipboard content
63
70
 
64
71
  `h save` saves whatever is on the macOS clipboard to `~/saved/` and prints the
@@ -196,7 +203,7 @@ See the [task command reference](docs/t.md) for all commands and the [workflow i
196
203
  | `h edit` | Open the h script in your editor |
197
204
  | `h alert` | macOS desktop notifications via terminal-notifier |
198
205
  | `h herdr` | Herdr task popups plus Vim-style pane focus, movement, resize, swap, and zoom controls (`h herdr install`) |
199
- | `h task` | Same program as `t`: task records, todos, worktrees (`t NAME tree new`), and Herdr workspaces |
206
+ | `h task` | Same program as `t`: task records, todos, worktrees (`t NAME tree new`), and Herdr workspaces; `t ui` is a tree TUI over tasks, subtasks, and todos |
200
207
 
201
208
  ### External Subcommands
202
209
 
@@ -215,13 +222,15 @@ See the [task command reference](docs/t.md) for all commands and the [workflow i
215
222
  | `h pane` | Herdr pane management |
216
223
  | `h plugin` | Manage hiiro plugins (list, edit, search) |
217
224
  | `h pr` | GitHub PR management via gh CLI |
218
- | `h project` | Project navigation with Herdr workspace management |
225
+ | [`h project`](docs/h-project.md) | Project navigation with Herdr workspace management; `h project ui` is the projects screen over every worktree, directory, and alias |
219
226
  | `h queue` | Claude prompt queue with Herdr-based task execution |
227
+ | `h registry` | Named resources by type with short aliases; the same rows as `h table` |
220
228
  | `h service` | Manage background dev services with env variations and groups |
221
229
  | `h file` | Track and open frequently-used files per app |
222
230
  | `h run` | Run dev tools (lint/test/format) against changed files |
223
231
  | `h session` | Compatibility commands for Herdr workspaces |
224
232
  | `h sha` | Extract short SHA from git log |
233
+ | [`h table`](docs/h-table.md) | TUI and scriptable CLI over registry tables (links, sounds, pins, ...) with task scope, tags, and per-table actions |
225
234
  | `h todo` | Todo list management with tags and task association |
226
235
  | `h window` | Compatibility commands for Herdr tabs |
227
236
  | `h wtree` | Git worktree management |
@@ -339,15 +348,18 @@ end
339
348
 
340
349
  ### Commands with declared options
341
350
 
342
- Use `add_cmd` to expose a command's options in help. Undeclared names in `opts:`
343
- become boolean flags with a default of `false`. A flag does not consume the next
344
- positional argument.
351
+ Use `add_cmd` to expose a command's options in help. Choose a declaration bucket
352
+ for each undeclared name:
353
+
354
+ - `opts:` creates a string-valued option with a default of `nil`.
355
+ - `flags:` creates a boolean flag with a default of `false`. Flags do not consume the next positional argument.
356
+ - `mopts:` creates a repeatable string-valued option with a default of `[]`.
345
357
 
346
358
  ```ruby
347
359
  Hiiro.run do
348
360
  add_option :task, short: :t, desc: 'Task name'
349
361
 
350
- add_cmd :test, opts: %i[a b c d task] do
362
+ add_cmd :test, opts: %i[task], flags: %i[a b c d], mopts: %i[tag] do
351
363
  puts 'a was set' if opts.a
352
364
  puts opts.task if opts.task
353
365
  puts opts.args
@@ -355,15 +367,20 @@ Hiiro.run do
355
367
  end
356
368
  ```
357
369
 
358
- Here, `test -a payload -t investigation` sets `opts.a` to `true`, leaves
359
- `payload` in `opts.args`, and sets `opts.task` to `investigation`.
370
+ Here, `test -a payload -t investigation --tag ruby --tag cli` sets `opts.a`
371
+ to `true`, leaves `payload` in `opts.args`, sets `opts.task` to
372
+ `investigation`, and sets `opts.tag` to `['ruby', 'cli']`.
360
373
  `test -h` and `test --help` display the selected options without running the
361
374
  command block.
362
375
 
363
- Automatic flags use their first letter as a short alias only when it is unique
364
- among the command's automatic flags and does not conflict with a selected
365
- explicit option or `-h`. Every automatic flag has a long form. Explicit flags
366
- and value options keep their definitions.
376
+ Names accept strings or symbols and are deduplicated across the buckets.
377
+ Existing definitions keep their type, default, short alias, and description,
378
+ regardless of the bucket used to select them. New definitions are
379
+ registered on the parent option set so later commands reuse them.
380
+
381
+ Automatic short aliases use the first unused alphanumeric character in the full
382
+ name, scanning left to right. All existing short aliases and `-h` are reserved;
383
+ aliases are never reassigned. A name with no available character has only its long form.
367
384
 
368
385
  For a command that delegates to `run_child`, use `add_cmd(..., passthrough: true)`.
369
386
  It forwards control without parsing the parent's arguments or intercepting the
@@ -433,12 +450,13 @@ All configuration lives in `~/.config/hiiro/`:
433
450
  ```
434
451
  ~/.config/hiiro/
435
452
  plugins/ # Plugin files (auto-loaded)
436
- pins/ # Pin storage (per command)
453
+ pins/ # Legacy pin files; imported once into registry tables named after the command (h-sha -> sha), then renamed .bak
454
+ sounds.yml # Legacy; imported once into registry table sound, then renamed .bak
437
455
  services/ # Service runtime state
438
456
  bg-history.txt # Background command history
439
457
  env_templates/ # Base .env templates for services
440
458
  tasks/ # Task metadata
441
- projects.yml # Project aliases
459
+ projects.yml # Legacy; imported once into registry table project, then renamed .bak (edit aliases with h project edit)
442
460
  apps.yml # App directory mappings
443
461
  services.yml # Service and service group definitions
444
462
  tools.yml # Runner tool definitions
data/bin/h-alias CHANGED
@@ -5,7 +5,7 @@ require 'hiiro'
5
5
  Hiiro.run do
6
6
  add_flag :global, short: :g, desc: 'Create a global zsh alias'
7
7
 
8
- add_cmd :add, args: ['NAME', 'COMMAND...'], opts: [:global] do
8
+ add_cmd :add, args: ['NAME', 'COMMAND...'], flags: [:global] do
9
9
  abort 'Usage: h alias add [--global|-g] NAME COMMAND...' unless opts.args.length >= 2
10
10
  name, *command_args = opts.args
11
11
  abort 'Invalid alias name' unless /\A[A-Za-z0-9_.][A-Za-z0-9_.-]*\z/.match?(name)
data/bin/h-date CHANGED
@@ -8,13 +8,13 @@ Hiiro.run(*ARGV) do
8
8
  add_flag(:utc, short: :u, desc: 'Output in UTC')
9
9
 
10
10
  # h date to_unix [date-string] -> unix seconds (or ms with --ms)
11
- add_cmd(:to_unix, :unix, :ts, opts: %i[ms]) do
11
+ add_cmd(:to_unix, :unix, :ts, flags: %i[ms]) do
12
12
  t = Hiiro::TimeInput.parse(@opts.args.join(' '))
13
13
  puts @opts.ms ? t.unix_milliseconds : t.unix_seconds
14
14
  end
15
15
 
16
16
  # h date from_unix <timestamp> -> human date (auto-detects ms by digit count)
17
- add_cmd(:from_unix, :parse, opts: %i[ms utc]) do
17
+ add_cmd(:from_unix, :parse, flags: %i[ms utc]) do
18
18
  raw = @opts.args.first or abort 'usage: h date from_unix <timestamp> [--ms] [--utc]'
19
19
  t = Hiiro::TimeInput.parse(raw, ms: @opts.ms ? true : nil)
20
20
  t = t.utc if @opts.utc
@@ -22,7 +22,7 @@ Hiiro.run(*ARGV) do
22
22
  end
23
23
 
24
24
  # h date fmt Y-m-d H:M:S -> strftime with % auto-inserted before each char
25
- add_cmd(:fmt, :format, :strftime, opts: %i[utc]) do
25
+ add_cmd(:fmt, :format, :strftime, flags: %i[utc]) do
26
26
  fmt = @opts.args.join(' ')
27
27
  abort 'usage: h date fmt <strftime-chars-without-percent>' if fmt.empty?
28
28
  with_percent = fmt.gsub(/[A-Za-z%]/) { |c| c == '%' ? '%%' : "%#{c}" }
@@ -30,20 +30,20 @@ Hiiro.run(*ARGV) do
30
30
  puts t.strftime(with_percent)
31
31
  end
32
32
 
33
- add_cmd(:now, opts: %i[utc]) do
33
+ add_cmd(:now, flags: %i[utc]) do
34
34
  t = @opts.utc ? Time.now.utc : Time.now
35
35
  puts t.iso8601
36
36
  end
37
37
 
38
38
  # h date dur <seconds> -> 1d23h12m10s (--ms to interpret input as milliseconds)
39
- add_cmd(:dur, :duration, :human, opts: %i[ms]) do
39
+ add_cmd(:dur, :duration, :human, flags: %i[ms]) do
40
40
  raw = @opts.args.first or abort 'usage: h date dur <seconds> [--ms]'
41
41
  n = Rational(raw.to_s)
42
42
  puts (@opts.ms ? Hiiro::Duration.milliseconds(n) : Hiiro::Duration.seconds(n)).compact
43
43
  end
44
44
 
45
45
  # h date diff <a> <b> -> how far apart two dates are (dates or unix timestamps)
46
- add_cmd(:diff, opts: %i[ms]) do
46
+ add_cmd(:diff, flags: %i[ms]) do
47
47
  a, b = @opts.args
48
48
  abort 'usage: h date diff <date-a> <date-b> (each: date string or unix timestamp)' unless a && b
49
49
  ta = Hiiro::TimeInput.parse(a, ms: @opts.ms ? true : nil)
data/bin/h-db CHANGED
@@ -188,7 +188,6 @@ Hiiro.run(*ARGV) do
188
188
  apps: %w[name],
189
189
  projects: %w[name],
190
190
  assignments: %w[worktree],
191
- pins: %w[command key],
192
191
  pane_homes: %w[name],
193
192
  check_runs: %w[pr_number name],
194
193
  reminders: %w[text],
data/bin/h-git CHANGED
@@ -215,7 +215,7 @@ Hiiro.run(*ARGV, plugins: [Pins]) do
215
215
  end
216
216
  end
217
217
 
218
- add_cmd(:commit, opts: %i[dry_run yes model]) do
218
+ add_cmd(:commit, opts: %i[model], flags: %i[dry_run yes]) do
219
219
  unless system('git', 'rev-parse', '--is-inside-work-tree', out: File::NULL, err: File::NULL)
220
220
  puts 'Not inside a git work tree.'
221
221
  next false