hiiro 0.1.352 → 0.1.353

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: 16ba8062d4d8f261771beb98b5baad695b43d05f6f0be4f8ebcc0f8e18a6e89b
4
- data.tar.gz: b756598368c4faa85ec332df703e4f2e859f84bcdf2c68319dc1b5ce48c6d7ab
3
+ metadata.gz: bbd7598e65b8521905ec67a960f09700d7260c5329e066e6cfcbb4661c180c99
4
+ data.tar.gz: 189ca275fa4cc2af210c1d67c8976bc4b90343e68eb4a0fe86ebedea1421bb9f
5
5
  SHA512:
6
- metadata.gz: 417c7bc005bbe71d9fc62a9cab04616c78005aad16f9604eb98f5aee6cfbe42a19bbbf789f04fb0a452c3ebea7e323155a7432d380f03f6b42cb0be3d4b66260
7
- data.tar.gz: 2b0b553966be3f3cfd4d457afa62b7c8021b9633d5eaa92ded65ad808d41256de9216ab495c474fcbad27c3a479f3ff530ad69fe4c6f91457d06f4db213946b6
6
+ metadata.gz: 50b6f3d077471531303d04f9ef234ff4ade40dbea85b52f575e6c9e3c48088ba1318dd0798eaeef15acdeb4ce0ca8fcf13100d4963fb5174dccdefaf87de228f
7
+ data.tar.gz: 4c852f5cea47667811dd2dd461b5c1bdfe3935cc7fecad2c95c7d3d43f28d3c7aea0554ea415cfda812e776b047bf4dee9ef77b34fdb0ffc5b806170864e2705
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.1.353] - 2026-04-26
6
+
7
+ ### Added
8
+ - `Hiiro::Tui::ListScreen` for building keyboard-driven full-screen list interfaces
9
+ - `Hiiro::Config.load_yaml` and `Hiiro::Config.yaml_dig` helpers for YAML file loading with nested key access
10
+
5
11
  ## [0.1.352] - 2026-04-24
6
12
 
7
13
  ### Added
@@ -323,4 +329,4 @@
323
329
  - `h db cleanup` subcommand to preview and prune duplicate rows from SQLite tables
324
330
 
325
331
  ### Fixed
326
- - Prevent duplicate pinned_prs during import with `insert_conflict` and per-row rescue
332
+ - Prevent duplicate pinned_prs during import with `insert_conflict` and per-row rescue
data/README.md CHANGED
@@ -8,6 +8,7 @@ A lightweight, extensible CLI framework for Ruby. Build your own multi-command t
8
8
  - **Abbreviation matching** - Type `h ex hel` instead of `h example hello`
9
9
  - **Plugin system** - Extend functionality with reusable modules
10
10
  - **Per-command storage** - Each command gets its own pin/config namespace
11
+ - **TUI helpers** - Build keyboard-driven list screens with `Hiiro::Tui::ListScreen`
11
12
 
12
13
  See [docs/](docs/) for detailed documentation on all subcommands.
13
14
 
@@ -154,6 +155,10 @@ h example hello # => Hi!
154
155
  h example bye # => Goodbye!
155
156
  ```
156
157
 
158
+ ### Building TUIs
159
+
160
+ Use `Hiiro::Tui::ListScreen` for simple full-screen list interfaces. Subclass it, override `header_lines`, `format_row`, and `handle_key`, then run it from a normal `Hiiro.run` subcommand.
161
+
157
162
  ### Method 2: Inline Subcommands
158
163
 
159
164
  Modify `exe/h` directly to add subcommands to the base `h` command:
data/lib/hiiro/config.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'yaml'
2
+
1
3
  class Hiiro
2
4
  class Config
3
5
  BASE_DIR = File.join(Dir.home, '.config/hiiro')
@@ -15,6 +17,36 @@ class Hiiro
15
17
  File.join(BASE_DIR, relpath)
16
18
  end
17
19
 
20
+ def load_yaml(relpath = 'config.yml', default: {}, permitted_classes: [Symbol])
21
+ file = path(relpath)
22
+ return default unless File.exist?(file)
23
+
24
+ YAML.safe_load_file(file, permitted_classes: permitted_classes) || default
25
+ rescue StandardError
26
+ default
27
+ end
28
+
29
+ def yaml_dig(relpath = 'config.yml', *keys, default: nil, permitted_classes: [Symbol])
30
+ value = load_yaml(relpath, default: {}, permitted_classes: permitted_classes)
31
+
32
+ keys.flatten.each do |key|
33
+ return default unless value.is_a?(Hash)
34
+
35
+ value =
36
+ if value.key?(key)
37
+ value[key]
38
+ elsif value.key?(key.to_s)
39
+ value[key.to_s]
40
+ elsif key.respond_to?(:to_sym) && value.key?(key.to_sym)
41
+ value[key.to_sym]
42
+ else
43
+ return default
44
+ end
45
+ end
46
+
47
+ value.nil? ? default : value
48
+ end
49
+
18
50
  def data_path(relpath='')
19
51
  File.join(DATA_DIR, relpath)
20
52
  end
data/lib/hiiro/tui.rb ADDED
@@ -0,0 +1,216 @@
1
+ require 'io/console'
2
+
3
+ class Hiiro
4
+ module Tui
5
+ module Terminal
6
+ def with_screen
7
+ input = $stdin
8
+ $stdout.write("\e[?1049h\e[?25l")
9
+ $stdout.flush
10
+
11
+ input.raw do
12
+ yield input
13
+ end
14
+ ensure
15
+ $stdout.write("\r\e[0m\e[?25h\e[?1049l")
16
+ $stdout.flush
17
+ end
18
+
19
+ def read_key(input)
20
+ char = input.getch
21
+ return :ctrl_c if char == "\u0003"
22
+ return :enter if char == "\r" || char == "\n"
23
+
24
+ if char == "\e"
25
+ first = read_escape_char(input)
26
+ return :escape if first.nil?
27
+ return :escape unless first == '['
28
+
29
+ case read_escape_char(input)
30
+ when 'A' then :up
31
+ when 'B' then :down
32
+ when 'C' then :right
33
+ when 'D' then :left
34
+ else :escape
35
+ end
36
+ else
37
+ char
38
+ end
39
+ end
40
+
41
+ def read_escape_char(input)
42
+ return nil unless IO.select([input], nil, nil, 0.02)
43
+
44
+ input.getch
45
+ end
46
+
47
+ def terminal_rows
48
+ env_dimension('LINES') || IO.console.winsize[0]
49
+ rescue StandardError
50
+ 24
51
+ end
52
+
53
+ def terminal_cols
54
+ env_dimension('COLUMNS') || IO.console.winsize[1]
55
+ rescue StandardError
56
+ 80
57
+ end
58
+
59
+ def terminal_line(text, cols)
60
+ truncate(text, cols) + "\e[K"
61
+ end
62
+
63
+ def center_text(text, cols)
64
+ truncated = truncate(text, cols)
65
+ return truncated if truncated.length >= cols
66
+
67
+ left_padding = [(cols - truncated.length) / 2, 0].max
68
+ (' ' * left_padding) + truncated
69
+ end
70
+
71
+ def truncate(text, cols)
72
+ return text if text.length <= cols
73
+ return text[0, cols] if cols <= 1
74
+
75
+ text[0, cols - 1] + '…'
76
+ end
77
+
78
+ def visible_text(text, cols, offset = 0)
79
+ return truncate(text, cols) if offset <= 0
80
+
81
+ clipped = text[offset..] || ''
82
+ return truncate(clipped, cols) if cols <= 1
83
+
84
+ truncate("«#{clipped}", cols)
85
+ end
86
+
87
+ def env_dimension(name)
88
+ value = ENV[name].to_i
89
+ return nil unless value.positive?
90
+
91
+ value
92
+ end
93
+ end
94
+
95
+ class ListScreen
96
+ include Terminal
97
+
98
+ attr_reader :items, :cursor, :top, :horizontal_offset
99
+
100
+ def initialize(items:, empty_message: 'No items.')
101
+ @items = items
102
+ @empty_message = empty_message
103
+ @cursor = 0
104
+ @top = 0
105
+ @horizontal_offset = 0
106
+ end
107
+
108
+ def run
109
+ if items.empty?
110
+ puts @empty_message
111
+ return false
112
+ end
113
+
114
+ with_screen do |input|
115
+ loop do
116
+ render
117
+
118
+ result = handle_key(read_key(input))
119
+ return result unless result == :continue
120
+ end
121
+ end
122
+ end
123
+
124
+ def handle_key(key)
125
+ case key
126
+ when :up, 'k'
127
+ move(-1)
128
+ when :down, 'j'
129
+ move(1)
130
+ when :left, 'h'
131
+ scroll_horizontal(-4)
132
+ when :right, 'l'
133
+ scroll_horizontal(4)
134
+ when 'q', :escape, :ctrl_c
135
+ return false
136
+ end
137
+
138
+ :continue
139
+ end
140
+
141
+ def render
142
+ rows = terminal_rows
143
+ cols = terminal_cols
144
+ line_cols = [cols - 1, 1].max
145
+ headers = header_lines
146
+ visible_rows = [rows - headers.length - footer_height, 1].max
147
+ visible = items[@top, visible_rows] || []
148
+ @horizontal_offset = [horizontal_offset, max_horizontal_offset(line_cols, visible)].min
149
+
150
+ lines = headers.map { |line| terminal_line(line, line_cols) }
151
+ visible.each_with_index do |item, idx|
152
+ lines << format_row(item, @top + idx == cursor, line_cols)
153
+ end
154
+
155
+ (visible_rows - visible.length).times { lines << terminal_line('', line_cols) }
156
+ footer_lines.each { |line| lines << terminal_line(line, line_cols) }
157
+
158
+ $stdout.write("\e[H\e[2J")
159
+ $stdout.write(lines.join("\r\n"))
160
+ $stdout.write("\r")
161
+ $stdout.flush
162
+ end
163
+
164
+ def header_lines
165
+ []
166
+ end
167
+
168
+ def footer_lines
169
+ ["Showing #{@top + 1}-#{@top + visible_items.length} of #{items.length}"]
170
+ end
171
+
172
+ def footer_height
173
+ 1
174
+ end
175
+
176
+ def format_row(item, current, line_cols)
177
+ prefix = current ? '> ' : ' '
178
+ style = current ? "\e[7m" : "\e[0m"
179
+ text = (prefix + visible_text(item.to_s, [line_cols - prefix.length, 1].max, horizontal_offset)).ljust(line_cols)
180
+
181
+ "#{style}#{text}\e[0m\e[K"
182
+ end
183
+
184
+ def move(delta)
185
+ @cursor = [[cursor + delta, 0].max, items.length - 1].min
186
+ visible_rows = body_rows_budget
187
+ @top = cursor if cursor < top
188
+ @top = cursor - visible_rows + 1 if cursor >= top + visible_rows
189
+ @horizontal_offset = [horizontal_offset, max_horizontal_offset].min
190
+ end
191
+
192
+ def scroll_horizontal(delta)
193
+ @horizontal_offset = [[horizontal_offset + delta, 0].max, max_horizontal_offset].min
194
+ end
195
+
196
+ def visible_items
197
+ items[top, body_rows_budget] || []
198
+ end
199
+
200
+ def body_rows_budget
201
+ [terminal_rows - header_lines.length - footer_height, 1].max
202
+ end
203
+
204
+ def max_horizontal_offset(line_cols = nil, visible = nil)
205
+ line_cols ||= [terminal_cols - 1, 1].max
206
+ visible ||= visible_items
207
+ return 0 if visible.empty?
208
+
209
+ longest_visible_item = visible.map { |item| item.to_s.length }.max || 0
210
+ min_visible_chars = [5, line_cols].min
211
+
212
+ [longest_visible_item - min_visible_chars, 0].max
213
+ end
214
+ end
215
+ end
216
+ end
data/lib/hiiro/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Hiiro
2
- VERSION = "0.1.352"
2
+ VERSION = "0.1.353"
3
3
  end
data/lib/hiiro.rb CHANGED
@@ -47,6 +47,7 @@ require_relative "hiiro/pane_home"
47
47
  require_relative "hiiro/pin_record"
48
48
  require_relative "hiiro/reminder"
49
49
  require_relative 'hiiro/registry'
50
+ require_relative 'hiiro/tui'
50
51
 
51
52
  class String
52
53
  def underscore(camel_cased_word=self)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hiiro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.352
4
+ version: 0.1.353
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joshua Toyota
@@ -377,6 +377,7 @@ files:
377
377
  - lib/hiiro/tmux/window.rb
378
378
  - lib/hiiro/tmux/windows.rb
379
379
  - lib/hiiro/todo.rb
380
+ - lib/hiiro/tui.rb
380
381
  - lib/hiiro/version.rb
381
382
  - notes
382
383
  - obsidian_slides.md