textbringer 25 → 27

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: 32054ff4c06164a8e49a6d82bde0d32fb2aed2e7fa1c842fa908fbb57abd5a6f
4
- data.tar.gz: 7cebf42f75471bce99359f177344d0ce141cf617e9f350ac341a06253d48a4e8
3
+ metadata.gz: be4c934800d0492229b1476d4541825533ff3e824490ad23ef331926ee8d6492
4
+ data.tar.gz: 9157e0ba963f1dd77eab7a50f929c91b1ae6a7689c1ad41cda22876c32d6c0fb
5
5
  SHA512:
6
- metadata.gz: a96f17982fa5b370357fc97bfd5e14617a0af7df230e5ad40071f0b751e82860ea0a01f025f8279aa6f38847d59484f159fa9244ed057f084d537c8f3c5741c7
7
- data.tar.gz: 0fd05fb7d8aa16e51550e5276d549547b2023fad1b06aadc278fc41215da145a249fe868f632371811a10ab7680e4e9aa74e4db653665c733f2132a61e58456a
6
+ metadata.gz: 3f70f40322ad8fed8ef2044839e3df7fee67922b1cfe975283516cf3a10fac6566248d0a5fbcc98ef319155302105c0c37d9f30fa602f6a3b3b2069dbf5a1314
7
+ data.tar.gz: 6348a9ecddb8423d4fb07b8291dcda1ce7945cf049e997770347deb309ad8c3a507bfb6815a5db2c7f277677680c4023793cf9fd221ea96bd4ed60e5863e68e6
data/README.ja.md CHANGED
@@ -58,6 +58,14 @@ Textbringerが依存するcurses.gemをインストールする前に、ncursesw
58
58
  mod_meta_key = alt
59
59
  mod_meta_mode = esc
60
60
 
61
+ ### True color (24ビットカラー)
62
+
63
+ 端末エミュレータがtrue colorに対応している場合、`TERM`に`xterm-direct`を設定すると24ビットカラーが有効になります。
64
+
65
+ $ TERM=xterm-direct txtb
66
+
67
+ テーマで指定されたhex色が256色パレットに近似されず、そのまま表示されるようになります。
68
+
61
69
  ### 東アジアの曖昧な文字幅
62
70
 
63
71
  [曖昧な文字](http://unicode.org/reports/tr11/#Ambiguous)を全角扱いするには、以下の設定を~/.textbringer.rbに記述してください。
@@ -91,6 +99,22 @@ xterm、 mlterm、screenにはそれぞれ独自の設定項目があります
91
99
 
92
100
  cjkwidth on
93
101
 
102
+ ## テーマ
103
+
104
+ `Alt+x load_theme RET` で[textbringer/themes](lib/textbringer/themes)または~/.textbringer/themes内のテーマを読み込むことができます。
105
+
106
+ ~/.textbringer.rbでテーマを設定することもできます。
107
+
108
+ ```ruby
109
+ load_theme "catppuccin"
110
+ ```
111
+
112
+ テーマが独自の背景色を設定している場合に、端末のデフォルト背景色を使いたいときは`load_theme`の後に以下を追加してください。
113
+
114
+ ```ruby
115
+ set_background_color "default"
116
+ ```
117
+
94
118
  ## プラグイン
95
119
 
96
120
  * [Mournmail](https://github.com/shugo/mournmail): 電子メールクライアント
data/README.md CHANGED
@@ -31,6 +31,10 @@ Install ncursesw before installing curses.gem, on which textbringer depends.
31
31
  $ sudo apt-get install libncursesw5-dev
32
32
  $ gem install curses
33
33
 
34
+ You need ffi to use clipboard commands on Windows.
35
+
36
+ $ gem install ffi
37
+
34
38
  ## Usage
35
39
 
36
40
  $ txtb
@@ -60,6 +64,14 @@ Add the following lines to ~/.mlterm/main.
60
64
  mod_meta_key = alt
61
65
  mod_meta_mode = esc
62
66
 
67
+ ### True color (24-bit color)
68
+
69
+ If your terminal emulator supports true color, set `TERM` to `xterm-direct` to enable 24-bit color.
70
+
71
+ $ TERM=xterm-direct txtb
72
+
73
+ This allows themes to display their exact hex colors instead of approximating to the 256-color palette.
74
+
63
75
  ### East asian ambiguous width
64
76
 
65
77
  Add the following line to ~/.textbringer.rb to treat
@@ -103,6 +115,22 @@ Add the following line to ~/.screenrc.
103
115
  * [textbringer-presentation](https://github.com/shugo/textbringer-presentation): a presentation tool
104
116
  * [textbringer-ghost_text](https://github.com/shugo/textbringer-ghost_text): a [GhostText](https://github.com/fregante/GhostText) plugin
105
117
 
118
+ ## Themes
119
+
120
+ Type `Alt+x load_theme RET` to load a theme from [textbringer/themes](lib/textbringer/themes) or ~/.textbringer/themes.
121
+
122
+ Themes can also be loaded in ~/.textbringer.rb.
123
+
124
+ ```ruby
125
+ load_theme "catppuccin"
126
+ ```
127
+
128
+ If a theme sets its own background color but you prefer to use the terminal's default background, add the following after `load_theme`:
129
+
130
+ ```ruby
131
+ set_background_color "default"
132
+ ```
133
+
106
134
  ## Development
107
135
 
108
136
  After checking out the repo, run `bundle install` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -22,6 +22,30 @@ module Textbringer
22
22
  "brightwhite" => 15
23
23
  }
24
24
 
25
+ DIRECT_COLOR_THRESHOLD = 256 * 256 * 256
26
+
27
+ # Canonical RGB values for basic ANSI colors (xterm defaults),
28
+ # packed as 0xRRGGBB for use in direct color mode.
29
+ BASIC_COLORS_RGB = {
30
+ "default" => -1,
31
+ "black" => 0x000000,
32
+ "red" => 0xCD0000,
33
+ "green" => 0x00CD00,
34
+ "yellow" => 0xCDCD00,
35
+ "blue" => 0x0000EE,
36
+ "magenta" => 0xCD00CD,
37
+ "cyan" => 0x00CDCD,
38
+ "white" => 0xE5E5E5,
39
+ "brightblack" => 0x7F7F7F,
40
+ "brightred" => 0xFF0000,
41
+ "brightgreen" => 0x00FF00,
42
+ "brightyellow" => 0xFFFF00,
43
+ "brightblue" => 0x5C5CFF,
44
+ "brightmagenta" => 0xFF00FF,
45
+ "brightcyan" => 0x00FFFF,
46
+ "brightwhite" => 0xFFFFFF
47
+ }
48
+
25
49
  RGBColor = Struct.new(:r, :g, :b, :number)
26
50
 
27
51
  ADDITIONAL_COLORS = []
@@ -46,10 +70,17 @@ module Textbringer
46
70
  end
47
71
  end
48
72
 
73
+ def self.direct_color?
74
+ Window.colors >= DIRECT_COLOR_THRESHOLD
75
+ end
76
+
49
77
  def self.find_color_number(name)
50
78
  if name.is_a?(Integer)
51
79
  return name
52
80
  end
81
+ if direct_color?
82
+ return find_direct_color(name)
83
+ end
53
84
  case name
54
85
  when /\A\#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})\z/
55
86
  r = $1.to_i(16)
@@ -65,5 +96,20 @@ module Textbringer
65
96
  BASIC_COLORS[name]
66
97
  end
67
98
  end
99
+
100
+ def self.find_direct_color(name)
101
+ case name
102
+ when /\A\#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})\z/
103
+ r = $1.to_i(16)
104
+ g = $2.to_i(16)
105
+ b = $3.to_i(16)
106
+ (r << 16) | (g << 8) | b
107
+ else
108
+ unless BASIC_COLORS_RGB.key?(name)
109
+ raise EditorError, "No such color: #{name}"
110
+ end
111
+ BASIC_COLORS_RGB[name]
112
+ end
113
+ end
68
114
  end
69
115
  end
@@ -2,12 +2,15 @@ module Clipboard
2
2
  @implementation = nil
3
3
  end
4
4
 
5
- require "clipboard"
6
-
7
5
  module Textbringer
8
6
  module Commands
9
- CLIPBOARD_AVAILABLE =
10
- Clipboard.implementation.name != "Clipboard::File"
7
+ begin
8
+ require "clipboard"
9
+ CLIPBOARD_AVAILABLE =
10
+ Clipboard.implementation.name != "Clipboard::File"
11
+ rescue LoadError
12
+ CLIPBOARD_AVAILABLE = false
13
+ end
11
14
 
12
15
  if CLIPBOARD_AVAILABLE
13
16
  GLOBAL_MAP.define_key("\M-w", :clipboard_copy_region)
@@ -291,46 +291,48 @@ module Textbringer
291
291
  if CONFIG[:shell_file_name]
292
292
  cmd = [CONFIG[:shell_file_name], CONFIG[:shell_command_switch], cmd]
293
293
  end
294
- Open3.popen3(*cmd, opts) do |input, output, error, wait_thread|
295
- input.close
296
- catch(:finish) do
297
- loop do
298
- rs, = IO.select([output, error], nil, nil, 0.5)
299
- Window.redisplay
300
- rs&.each do |r|
301
- begin
302
- s = r.read_nonblock(1024).force_encoding("utf-8").
303
- scrub("\u{3013}").gsub(/\r\n/, "\n")
304
- buffer.insert(s)
305
- Window.redisplay
306
- rescue EOFError
307
- throw(:finish) if output.eof? && error.eof?
308
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK
309
- Window.redisplay
310
- next
294
+ with_clean_env do |env|
295
+ Open3.popen3(env, *cmd, opts) do |input, output, error, wait_thread|
296
+ input.close
297
+ catch(:finish) do
298
+ loop do
299
+ rs, = IO.select([output, error], nil, nil, 0.5)
300
+ Window.redisplay
301
+ rs&.each do |r|
302
+ begin
303
+ s = r.read_nonblock(1024).force_encoding("utf-8").
304
+ scrub("\u{3013}").gsub(/\r\n/, "\n")
305
+ buffer.insert(s)
306
+ Window.redisplay
307
+ rescue EOFError
308
+ throw(:finish) if output.eof? && error.eof?
309
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
310
+ Window.redisplay
311
+ next
312
+ end
311
313
  end
312
- end
313
- if received_keyboard_quit?
314
- if signals.empty?
315
- keyboard_quit
316
- else
317
- sig = signals.shift
318
- pid = wait_thread.pid
319
- pid = -pid if /mswin32|mingw32/ !~ RUBY_PLATFORM
320
- message("Send #{sig} to #{pid}")
321
- Process.kill(sig, pid)
314
+ if received_keyboard_quit?
315
+ if signals.empty?
316
+ keyboard_quit
317
+ else
318
+ sig = signals.shift
319
+ pid = wait_thread.pid
320
+ pid = -pid if /mswin32|mingw32/ !~ RUBY_PLATFORM
321
+ message("Send #{sig} to #{pid}")
322
+ Process.kill(sig, pid)
323
+ end
322
324
  end
323
325
  end
324
326
  end
325
- end
326
- status = wait_thread.value
327
- pid = status.pid
328
- if status.exited?
329
- code = status.exitstatus
330
- message("Process #{pid} exited with status code #{code}")
331
- elsif status.signaled?
332
- signame = Signal.signame(status.termsig)
333
- message("Process #{pid} was killed by #{signame}")
327
+ status = wait_thread.value
328
+ pid = status.pid
329
+ if status.exited?
330
+ code = status.exitstatus
331
+ message("Process #{pid} exited with status code #{code}")
332
+ elsif status.signaled?
333
+ signame = Signal.signame(status.termsig)
334
+ message("Process #{pid} was killed by #{signame}")
335
+ end
334
336
  end
335
337
  end
336
338
  ensure
@@ -352,6 +354,36 @@ module Textbringer
352
354
  RubyVM::MJIT.resume
353
355
  end
354
356
 
357
+ define_command(:forward_page,
358
+ doc: "Move forward to page boundary.") do
359
+ |n = number_prefix_arg|
360
+ buffer = Buffer.current
361
+ n.times do
362
+ unless buffer.re_search_forward(/^\f/, raise_error: false)
363
+ buffer.end_of_buffer
364
+ break
365
+ end
366
+ end
367
+ end
368
+
369
+ define_command(:backward_page,
370
+ doc: "Move backward to page boundary.") do
371
+ |n = number_prefix_arg|
372
+ buffer = Buffer.current
373
+ n.times do
374
+ if buffer.point == 0
375
+ break
376
+ end
377
+ buffer.backward_char
378
+ if buffer.re_search_backward(/^\f/, raise_error: false)
379
+ buffer.forward_char
380
+ else
381
+ buffer.beginning_of_buffer
382
+ break
383
+ end
384
+ end
385
+ end
386
+
355
387
  define_command(:what_cursor_position,
356
388
  doc: "Print info on cursor position.") do
357
389
  |arg = current_prefix_arg|
@@ -7,6 +7,8 @@ module Textbringer
7
7
  @@face_table = {}
8
8
  @@next_color_pair = 1
9
9
  @@color_pair_cache = {}
10
+ @@default_foreground = -1
11
+ @@default_background = -1
10
12
 
11
13
  def self.[](name)
12
14
  @@face_table[name]
@@ -26,6 +28,20 @@ module Textbringer
26
28
  @@face_table.delete(name)
27
29
  end
28
30
 
31
+ # Set the concrete color numbers substituted for -1 (default color).
32
+ # PDCurses resolves -1 when init_pair is called and
33
+ # assume_default_colors only affects color pair 0, so faces must be
34
+ # re-resolved with concrete colors to get the new default colors.
35
+ def self.set_default_color_numbers(fg_num, bg_num)
36
+ return if fg_num == @@default_foreground &&
37
+ bg_num == @@default_background
38
+ @@default_foreground = fg_num
39
+ @@default_background = bg_num
40
+ @@face_table.each_value do |face|
41
+ face.send(:resolve_inheritance)
42
+ end
43
+ end
44
+
29
45
  def initialize(name, **opts)
30
46
  @name = name
31
47
  update(**opts)
@@ -68,6 +84,8 @@ module Textbringer
68
84
  @reverse = @explicit_reverse.nil? ? (parent&.instance_variable_get(:@reverse) || false) : @explicit_reverse
69
85
  fg_num = Color[@foreground]
70
86
  bg_num = Color[@background]
87
+ fg_num = @@default_foreground if fg_num == -1
88
+ bg_num = @@default_background if bg_num == -1
71
89
  key = [fg_num, bg_num]
72
90
  unless @@color_pair_cache.key?(key)
73
91
  @@color_pair_cache[key] = @@next_color_pair
@@ -307,7 +307,7 @@ module Textbringer
307
307
  when "x"
308
308
  prev_candidate
309
309
  nil
310
- when "\C-m", "\r", "\n"
310
+ when "\r", "\n"
311
311
  confirm_selecting
312
312
  nil
313
313
  else
@@ -167,6 +167,8 @@ module Textbringer
167
167
  GLOBAL_MAP.define_key(:npage, :scroll_up)
168
168
  GLOBAL_MAP.define_key("\M-v", :scroll_down)
169
169
  GLOBAL_MAP.define_key(:ppage, :scroll_down)
170
+ GLOBAL_MAP.define_key("\C-x]", :forward_page)
171
+ GLOBAL_MAP.define_key("\C-x[", :backward_page)
170
172
  GLOBAL_MAP.define_key("\C-x0", :delete_window)
171
173
  GLOBAL_MAP.define_key("\C-x1", :delete_other_windows)
172
174
  GLOBAL_MAP.define_key("\C-x2", :split_window)
@@ -188,7 +188,6 @@ module Textbringer
188
188
  doc: "Delete files flagged for deletion.") do
189
189
  files = collect_flagged_files
190
190
  return if files.empty?
191
- list = files.map { |f| " #{f}" }.join("\n")
192
191
  if yes_or_no?("Delete these files?")
193
192
  files.each do |name|
194
193
  next if name == "." || name == ".."
@@ -8,7 +8,7 @@ module Textbringer
8
8
  class RubyMode < ProgrammingMode
9
9
  self.file_name_pattern =
10
10
  /\A(?:.*\.(?:rb|ru|rake|thor|jbuilder|gemspec|podspec)|
11
- (?:Gem|Rake|Cap|Thor|Vagrant|Guard|Pod)file)\z/ix
11
+ (?:Gem|Rake|Cap|Thor|Vagrant|Guard|Pod)file|.irbrc)\z/ix
12
12
  self.interpreter_name_pattern = /ruby/i
13
13
 
14
14
  def comment_start
@@ -22,18 +22,13 @@ module Textbringer
22
22
  @prism_version = nil
23
23
  @prism_tokens = nil
24
24
  @prism_ast = nil
25
- @prism_method_call_locs = nil
25
+ @prism_method_name_locs = nil
26
26
  @literal_levels = nil
27
27
  @literal_levels_version = nil
28
28
  end
29
29
 
30
30
  def forward_definition(n = number_prefix_arg || 1)
31
- ensure_prism_tokens
32
- tokens = @prism_tokens.filter_map { |token, _state|
33
- type = token.type
34
- next if type == :EOF
35
- [token.location.start_line, type]
36
- }
31
+ tokens = filter_prism_tokens_line_and_type
37
32
  @buffer.forward_line
38
33
  n.times do |i|
39
34
  tokens = tokens.drop_while { |l, type|
@@ -54,12 +49,7 @@ module Textbringer
54
49
  end
55
50
 
56
51
  def backward_definition(n = number_prefix_arg || 1)
57
- ensure_prism_tokens
58
- tokens = @prism_tokens.filter_map { |token, _state|
59
- type = token.type
60
- next if type == :EOF
61
- [token.location.start_line, type]
62
- }.reverse
52
+ tokens = filter_prism_tokens_line_and_type.reverse
63
53
  @buffer.beginning_of_line
64
54
  n.times do |i|
65
55
  tokens = tokens.drop_while { |l, type|
@@ -120,12 +110,11 @@ module Textbringer
120
110
  def highlight(ctx)
121
111
  ensure_prism_tokens
122
112
  return unless @prism_tokens
123
- ensure_method_call_locs
113
+ ensure_method_name_locs
124
114
  base_pos = ctx.buffer.point_min
125
115
  hl_start = ctx.highlight_start
126
116
  hl_end = ctx.highlight_end
127
117
  in_symbol = false
128
- after_def = false
129
118
  after_class_or_module = false
130
119
  @prism_tokens.each do |token_info|
131
120
  token = token_info[0]
@@ -139,26 +128,18 @@ module Textbringer
139
128
  face_name = PRISM_TOKEN_FACES[type]
140
129
  if in_symbol
141
130
  face_name = :string if face_name.nil? || face_name == :constant ||
142
- face_name == :operator
143
- elsif after_def
144
- face_name = :function_name if type == :IDENTIFIER ||
145
- type == :CONSTANT || type == :METHOD_NAME ||
146
- PRISM_TOKEN_FACES[type] == :operator
131
+ face_name == :keyword || face_name == :operator
132
+ elsif @prism_method_name_locs.key?(offset)
133
+ face_name = :function_name
147
134
  elsif face_name == :constant &&
148
135
  (after_class_or_module || token.location.slice.match?(/\p{Lower}/))
149
136
  face_name = :type
150
- elsif @prism_method_call_locs.key?(offset)
151
- face_name = :function_name
152
137
  end
153
138
  if face_name && (face = Face[face_name])
154
139
  ctx.highlight(pos, pos_end, face)
155
140
  end
156
141
  end
157
142
  in_symbol = type == :SYMBOL_BEGIN
158
- after_def = type == :KEYWORD_DEF ||
159
- (after_def && (type == :KEYWORD_SELF || type == :DOT ||
160
- type == :NEWLINE || type == :IGNORED_NEWLINE ||
161
- type == :COMMENT))
162
143
  after_class_or_module = (type == :KEYWORD_CLASS || type == :KEYWORD_MODULE) ||
163
144
  (after_class_or_module && !(type == :NEWLINE || type == :SEMICOLON))
164
145
  end
@@ -199,6 +180,9 @@ module Textbringer
199
180
  # Strings and string-like
200
181
  STRING_BEGIN: :string, STRING_CONTENT: :string, STRING_END: :string,
201
182
  SYMBOL_BEGIN: :string, REGEXP_BEGIN: :string, REGEXP_END: :string,
183
+ PERCENT_LOWER_I: :string, PERCENT_UPPER_I: :string,
184
+ PERCENT_LOWER_W: :string, PERCENT_UPPER_W: :string,
185
+ PERCENT_LOWER_X: :string,
202
186
  HEREDOC_START: :string, HEREDOC_END: :string,
203
187
  LABEL: :property,
204
188
 
@@ -273,6 +257,8 @@ module Textbringer
273
257
  EQUAL_GREATER
274
258
  ].to_set
275
259
 
260
+ OPERATORS = %i(!= !~ =~ == === <=> > >= < <= & | ^ >> << - + % / * ** -@ +@ ~ ! [] []=)
261
+
276
262
  BLOCK_END = {
277
263
  EMBEXPR_BEGIN: :EMBEXPR_END,
278
264
  BRACE_LEFT: :BRACE_RIGHT,
@@ -489,6 +475,15 @@ module Textbringer
489
475
  @literal_levels[i][1] > 0
490
476
  end
491
477
 
478
+ def filter_prism_tokens_line_and_type
479
+ ensure_prism_tokens
480
+ @prism_tokens.filter_map { |token, _state|
481
+ type = token.type
482
+ next if type == :EOF
483
+ [token.location.start_line, type]
484
+ }
485
+ end
486
+
492
487
  def ensure_prism_tokens
493
488
  return if @prism_version == @buffer.version
494
489
  source = @buffer.to_s
@@ -499,27 +494,41 @@ module Textbringer
499
494
  @prism_ast = nil
500
495
  @prism_tokens = []
501
496
  end
502
- @prism_method_call_locs = nil
497
+ @prism_method_name_locs = nil
503
498
  @prism_version = @buffer.version
504
499
  @literal_levels_version = nil
505
500
  end
506
501
 
507
- def ensure_method_call_locs
508
- return if @prism_method_call_locs
509
- @prism_method_call_locs = {}
502
+ def ensure_method_name_locs
503
+ return if @prism_method_name_locs
504
+ @prism_method_name_locs = {}
510
505
  return unless @prism_ast
511
- collect_method_call_locs(@prism_ast)
506
+ collect_method_name_locs(@prism_ast)
512
507
  end
513
508
 
514
- def collect_method_call_locs(node)
515
- if node.is_a?(Prism::CallNode) && node.message_loc
516
- name_str = node.name.to_s
517
- unless name_str.match?(/\A[^a-zA-Z_]/) || name_str.end_with?("@")
518
- loc = node.message_loc
519
- @prism_method_call_locs[loc.start_offset] = true
520
- end
509
+ def collect_method_name_locs(node)
510
+ if node.type == :def_node
511
+ @prism_method_name_locs[node.name_loc.start_offset] = true
512
+ elsif node.type == :alias_method_node
513
+ add_alias_method_name_locs(node.new_name)
514
+ add_alias_method_name_locs(node.old_name)
515
+ elsif ((node.type == :call_node &&
516
+ !(node.call_operator_loc.nil? && OPERATORS.include?(node.name)) && # exclude operators
517
+ !((node.call_operator_loc.nil? || node.call_operator_loc.slice == "::") && # exclude constants
518
+ /\A\p{Upper}/.match?(node.name))) ||
519
+ node.type == :call_operator_write_node ||
520
+ node.type == :call_and_write_node ||
521
+ node.type == :call_or_write_node) &&
522
+ node.message_loc
523
+ @prism_method_name_locs[node.message_loc.start_offset] = true
524
+ end
525
+ node.compact_child_nodes.each { |child| collect_method_name_locs(child) }
526
+ end
527
+
528
+ def add_alias_method_name_locs(node)
529
+ if node.type == :symbol_node && node.opening_loc.nil?
530
+ @prism_method_name_locs[node.value_loc.start_offset] = true
521
531
  end
522
- node.compact_child_nodes.each { |child| collect_method_call_locs(child) }
523
532
  end
524
533
 
525
534
  def ensure_literal_levels