shoko 0.1.1 → 0.1.3
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 +4 -4
- data/.bundle/config +1 -2
- data/bin/shoko +10 -6
- data/bin/start +10 -6
- data/lib/shoko/adapters/input/annotations/mouse_handler.rb +57 -7
- data/lib/shoko/adapters/output/terminal/input/decoder.rb +34 -0
- data/lib/shoko/application/controllers/mouseable_reader.rb +50 -6
- data/lib/shoko/application/selectors/reader_selectors.rb +6 -2
- data/lib/shoko/core/services/coordinate_service.rb +1 -1
- data/lib/shoko/shared/version.rb +1 -1
- metadata +15 -2
- data/shoko-0.1.0.gem +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5060375164152dd31808e0548f1a525246af0d4708e037eb3590423730d4d6c6
|
|
4
|
+
data.tar.gz: a71558d1f38594e40ffe44d80f4d05dcdd1d1c43a23bb003ab42f74c9ffffeec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89e5372ed7369bb5631b4c190311303bbe8285602d5d7520060dc91f3c176c903f269a491d8b436f2c1d280717ddffd8b3498560aab076d7f7b7598ff76be0e8
|
|
7
|
+
data.tar.gz: b6ca5f48134e099a7b112c517bfa7efeb683606e3d25e66cdc27c8e6c6711615f49a7d2d7c4c75b7ef801e2d4286b3be8f3911dd00d726598093ee583c562198
|
data/.bundle/config
CHANGED
data/bin/shoko
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
app_root = File.expand_path('..', __dir__)
|
|
5
|
+
lib_dir = File.join(app_root, 'lib')
|
|
6
|
+
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
|
|
5
7
|
|
|
6
8
|
begin
|
|
7
|
-
|
|
9
|
+
gemspec = Dir[File.join(app_root, '*.gemspec')].first
|
|
10
|
+
if gemspec && File.exist?(File.join(app_root, 'Gemfile'))
|
|
11
|
+
require 'bundler/setup'
|
|
12
|
+
end
|
|
8
13
|
rescue LoadError
|
|
9
|
-
#
|
|
14
|
+
# Running without Bundler (e.g., installed gem).
|
|
10
15
|
end
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
require "shoko"
|
|
17
|
+
require 'shoko'
|
|
14
18
|
|
|
15
|
-
Shoko::CLI.run
|
|
19
|
+
Shoko::CLI.run
|
data/bin/start
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
app_root = File.expand_path('..', __dir__)
|
|
5
|
+
lib_dir = File.join(app_root, 'lib')
|
|
6
|
+
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)
|
|
5
7
|
|
|
6
8
|
begin
|
|
7
|
-
|
|
9
|
+
gemspec = Dir[File.join(app_root, '*.gemspec')].first
|
|
10
|
+
if gemspec && File.exist?(File.join(app_root, 'Gemfile'))
|
|
11
|
+
require 'bundler/setup'
|
|
12
|
+
end
|
|
8
13
|
rescue LoadError
|
|
9
|
-
#
|
|
14
|
+
# Running without Bundler (e.g., installed gem).
|
|
10
15
|
end
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
require "shoko"
|
|
17
|
+
require 'shoko'
|
|
14
18
|
|
|
15
|
-
Shoko::CLI.run
|
|
19
|
+
Shoko::CLI.run
|
|
@@ -4,22 +4,34 @@ module Shoko
|
|
|
4
4
|
module Adapters::Input::Annotations
|
|
5
5
|
# Handles mouse events for text selection in the reader
|
|
6
6
|
class MouseHandler
|
|
7
|
+
SGR_REGEX = /\e\[<(\d+);(\d+);(\d+)([Mm])/
|
|
8
|
+
|
|
7
9
|
attr_reader :selection_start, :selection_end, :selecting
|
|
8
10
|
|
|
9
11
|
def initialize
|
|
10
12
|
reset
|
|
11
13
|
end
|
|
12
14
|
|
|
15
|
+
def mouse_sequence?(input)
|
|
16
|
+
!parse_mouse_event(input).nil?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def mouse_prefix?(input)
|
|
20
|
+
bytes = input.to_s.b
|
|
21
|
+
return false if bytes.bytesize < 2
|
|
22
|
+
return false unless bytes.getbyte(0) == 0x1B && bytes.getbyte(1) == 0x5B
|
|
23
|
+
|
|
24
|
+
return true if bytes.bytesize == 2
|
|
25
|
+
|
|
26
|
+
[0x3C, 0x4D].include?(bytes.getbyte(2))
|
|
27
|
+
end
|
|
28
|
+
|
|
13
29
|
# Parse ANSI mouse event
|
|
14
30
|
def parse_mouse_event(input)
|
|
15
|
-
return
|
|
31
|
+
return parse_sgr_mouse_event(input) if sgr_mouse_sequence?(input)
|
|
32
|
+
return parse_x10_mouse_event(input) if x10_mouse_sequence?(input)
|
|
16
33
|
|
|
17
|
-
|
|
18
|
-
button: ::Regexp.last_match(1).to_i,
|
|
19
|
-
x: ::Regexp.last_match(2).to_i - 1, # Convert to 0-based
|
|
20
|
-
y: ::Regexp.last_match(3).to_i - 1,
|
|
21
|
-
released: ::Regexp.last_match(4) == 'm',
|
|
22
|
-
}
|
|
34
|
+
nil
|
|
23
35
|
end
|
|
24
36
|
|
|
25
37
|
# Handle mouse event and update selection state
|
|
@@ -63,6 +75,44 @@ module Shoko
|
|
|
63
75
|
|
|
64
76
|
private
|
|
65
77
|
|
|
78
|
+
def sgr_mouse_sequence?(input)
|
|
79
|
+
bytes = input.to_s.b
|
|
80
|
+
bytes.bytesize >= 4 && bytes.getbyte(0) == 0x1B && bytes.getbyte(1) == 0x5B && bytes.getbyte(2) == 0x3C
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def x10_mouse_sequence?(input)
|
|
84
|
+
bytes = input.to_s.b
|
|
85
|
+
bytes.bytesize >= 6 && bytes.getbyte(0) == 0x1B && bytes.getbyte(1) == 0x5B && bytes.getbyte(2) == 0x4D
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def parse_sgr_mouse_event(input)
|
|
89
|
+
match = SGR_REGEX.match(input)
|
|
90
|
+
return nil unless match
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
button: match[1].to_i,
|
|
94
|
+
x: match[2].to_i - 1, # Convert to 0-based
|
|
95
|
+
y: match[3].to_i - 1,
|
|
96
|
+
released: match[4] == 'm',
|
|
97
|
+
}
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def parse_x10_mouse_event(input)
|
|
101
|
+
bytes = input.to_s.b
|
|
102
|
+
cb = bytes.getbyte(3) - 32
|
|
103
|
+
cx = bytes.getbyte(4) - 33
|
|
104
|
+
cy = bytes.getbyte(5) - 33
|
|
105
|
+
|
|
106
|
+
return nil if cb.negative? || cx.negative? || cy.negative?
|
|
107
|
+
|
|
108
|
+
{
|
|
109
|
+
button: cb,
|
|
110
|
+
x: cx,
|
|
111
|
+
y: cy,
|
|
112
|
+
released: (cb & 3) == 3,
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
|
|
66
116
|
def start_selection(col, row)
|
|
67
117
|
@selecting = true
|
|
68
118
|
@selection_start = { x: col, y: row }
|
|
@@ -302,6 +302,13 @@ module Shoko
|
|
|
302
302
|
end
|
|
303
303
|
|
|
304
304
|
def parse_csi_sequence(prefix_bytes:, output_prefix:)
|
|
305
|
+
if x10_mouse_prefix?(prefix_bytes)
|
|
306
|
+
min_length = prefix_bytes == 1 ? 5 : 6
|
|
307
|
+
return nil if @buffer.bytesize < min_length
|
|
308
|
+
|
|
309
|
+
return parse_x10_mouse_sequence(prefix_bytes)
|
|
310
|
+
end
|
|
311
|
+
|
|
305
312
|
return nil unless (final_index = DecoderScanner.new(@buffer).csi_final_index(prefix_bytes))
|
|
306
313
|
|
|
307
314
|
end_index = final_index + 1
|
|
@@ -326,6 +333,33 @@ module Shoko
|
|
|
326
333
|
prefix ? "#{prefix}#{char}" : char
|
|
327
334
|
end
|
|
328
335
|
|
|
336
|
+
def x10_mouse_prefix?(prefix_bytes)
|
|
337
|
+
case prefix_bytes
|
|
338
|
+
when 2
|
|
339
|
+
return false unless @buffer.bytesize >= 3
|
|
340
|
+
|
|
341
|
+
@buffer.getbyte(0) == ESC && @buffer.getbyte(1) == 0x5B && @buffer.getbyte(2) == 0x4D
|
|
342
|
+
when 1
|
|
343
|
+
return false unless @buffer.bytesize >= 2
|
|
344
|
+
|
|
345
|
+
@buffer.getbyte(0) == CSI_8BIT && @buffer.getbyte(1) == 0x4D
|
|
346
|
+
else
|
|
347
|
+
false
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def parse_x10_mouse_sequence(prefix_bytes)
|
|
352
|
+
length = prefix_bytes + 1 + 3
|
|
353
|
+
raw = @buffer.byteslice(0, length)
|
|
354
|
+
consume_and_clear(length)
|
|
355
|
+
if prefix_bytes == 1
|
|
356
|
+
coords = raw.byteslice(2, 3) || ''.b
|
|
357
|
+
return "\e[M".b + coords
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
raw.force_encoding(Encoding::BINARY)
|
|
361
|
+
end
|
|
362
|
+
|
|
329
363
|
def degrade_pending_token
|
|
330
364
|
lead_byte = @buffer.getbyte(0)
|
|
331
365
|
consume_and_clear(1)
|
|
@@ -23,6 +23,7 @@ module Shoko
|
|
|
23
23
|
@coordinate_service = dependencies.resolve(:coordinate_service)
|
|
24
24
|
|
|
25
25
|
@mouse_handler = Shoko::Adapters::Input::Annotations::MouseHandler.new
|
|
26
|
+
@mouse_input_buffer = nil
|
|
26
27
|
@sidebar_scroll_drag_active = false
|
|
27
28
|
state.dispatch(Application::Actions::ClearPopupMenuAction.new)
|
|
28
29
|
@selected_text = nil
|
|
@@ -42,17 +43,60 @@ module Shoko
|
|
|
42
43
|
key = terminal_service.read_input_with_mouse(timeout: timeout)
|
|
43
44
|
return [] unless key
|
|
44
45
|
|
|
45
|
-
if key.start_with?("\e[<")
|
|
46
|
-
handle_mouse_input(key)
|
|
47
|
-
return []
|
|
48
|
-
end
|
|
49
|
-
|
|
50
46
|
keys = [key]
|
|
51
47
|
while (extra = terminal_service.read_key)
|
|
52
48
|
keys << extra
|
|
53
49
|
break if keys.size > 10
|
|
54
50
|
end
|
|
55
|
-
|
|
51
|
+
|
|
52
|
+
filter_mouse_sequences(keys)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def filter_mouse_sequences(keys)
|
|
56
|
+
remaining = []
|
|
57
|
+
saw_mouse = false
|
|
58
|
+
saw_mouse_prefix = false
|
|
59
|
+
|
|
60
|
+
keys.each do |token|
|
|
61
|
+
if @mouse_input_buffer
|
|
62
|
+
@mouse_input_buffer << token
|
|
63
|
+
if @mouse_handler.mouse_sequence?(@mouse_input_buffer)
|
|
64
|
+
handle_mouse_input(@mouse_input_buffer)
|
|
65
|
+
@mouse_input_buffer = nil
|
|
66
|
+
saw_mouse = true
|
|
67
|
+
next
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if @mouse_handler.mouse_prefix?(@mouse_input_buffer)
|
|
71
|
+
saw_mouse_prefix = true
|
|
72
|
+
next
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
remaining << @mouse_input_buffer
|
|
76
|
+
@mouse_input_buffer = nil
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if @mouse_handler.mouse_sequence?(token)
|
|
81
|
+
handle_mouse_input(token)
|
|
82
|
+
saw_mouse = true
|
|
83
|
+
next
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if @mouse_handler.mouse_prefix?(token)
|
|
87
|
+
@mouse_input_buffer = String(token)
|
|
88
|
+
saw_mouse_prefix = true
|
|
89
|
+
next
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
if saw_mouse || saw_mouse_prefix
|
|
93
|
+
next if token == 'q' || token == "\e"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
remaining << token
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
remaining
|
|
56
100
|
end
|
|
57
101
|
|
|
58
102
|
def handle_mouse_input(input)
|
|
@@ -122,10 +122,14 @@ module Shoko
|
|
|
122
122
|
rescue StandardError
|
|
123
123
|
nil
|
|
124
124
|
end
|
|
125
|
+
|
|
125
126
|
lines = registry&.lines
|
|
126
|
-
return lines if lines
|
|
127
|
+
return lines if lines.is_a?(Hash)
|
|
128
|
+
|
|
129
|
+
fallback = state.get(%i[reader rendered_lines])
|
|
130
|
+
return {} if fallback == :render_registry
|
|
127
131
|
|
|
128
|
-
|
|
132
|
+
fallback.is_a?(Hash) ? fallback : {}
|
|
129
133
|
end
|
|
130
134
|
|
|
131
135
|
def self.popup_menu(state)
|
data/lib/shoko/shared/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: shoko
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shoko
|
|
@@ -9,6 +9,20 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: base64
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: rexml
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -344,7 +358,6 @@ files:
|
|
|
344
358
|
- lib/shoko/test_support/terminal_double.rb
|
|
345
359
|
- lib/shoko/test_support/test_mode.rb
|
|
346
360
|
- lib/zip.rb
|
|
347
|
-
- shoko-0.1.0.gem
|
|
348
361
|
- zip.rb
|
|
349
362
|
homepage: https://sr.ht/~shayan/Shoko/
|
|
350
363
|
licenses:
|
data/shoko-0.1.0.gem
DELETED
|
Binary file
|