textbringer 19 → 24
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/CLAUDE.md +0 -6
- data/lib/textbringer/buffer.rb +2 -0
- data/lib/textbringer/commands/dired.rb +31 -0
- data/lib/textbringer/commands/files.rb +4 -0
- data/lib/textbringer/commands/gamegrid.rb +25 -0
- data/lib/textbringer/commands/tetris.rb +10 -0
- data/lib/textbringer/face.rb +6 -6
- data/lib/textbringer/faces/dired.rb +6 -0
- data/lib/textbringer/faces/gamegrid.rb +23 -0
- data/lib/textbringer/floating_window.rb +15 -40
- data/lib/textbringer/gamegrid.rb +164 -0
- data/lib/textbringer/input_method.rb +6 -0
- data/lib/textbringer/input_methods/skk_input_method.rb +132 -22
- data/lib/textbringer/keymap.rb +1 -0
- data/lib/textbringer/modes/dired_mode.rb +322 -0
- data/lib/textbringer/modes/gamegrid_mode.rb +46 -0
- data/lib/textbringer/modes/tetris_mode.rb +316 -0
- data/lib/textbringer/modes/transient_mark_mode.rb +2 -2
- data/lib/textbringer/version.rb +1 -1
- data/lib/textbringer/window.rb +71 -88
- data/lib/textbringer.rb +7 -0
- metadata +11 -2
|
@@ -1,4 +1,13 @@
|
|
|
1
|
+
require "open-uri"
|
|
2
|
+
require "fileutils"
|
|
3
|
+
require "socket"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
1
6
|
module Textbringer
|
|
7
|
+
CONFIG[:skk_dictionary_path] = File.expand_path("~/.textbringer/skk/SKK-JISYO.L")
|
|
8
|
+
CONFIG[:skk_server_host] = nil # nil = disabled (default)
|
|
9
|
+
CONFIG[:skk_server_port] = 1178
|
|
10
|
+
|
|
2
11
|
class SkkInputMethod < InputMethod
|
|
3
12
|
HIRAGANA_TABLE = {
|
|
4
13
|
"a" => "あ", "i" => "い", "u" => "う", "e" => "え", "o" => "お",
|
|
@@ -41,7 +50,11 @@ module Textbringer
|
|
|
41
50
|
"xtu" => "っ", "xtsu" => "っ",
|
|
42
51
|
"xya" => "ゃ", "xyu" => "ゅ", "xyo" => "ょ",
|
|
43
52
|
"xa" => "ぁ", "xi" => "ぃ", "xu" => "ぅ", "xe" => "ぇ", "xo" => "ぉ",
|
|
44
|
-
"," => "、", "." => "。",
|
|
53
|
+
"-" => "ー", "," => "、", "." => "。", "[" => "「", "]" => "」",
|
|
54
|
+
"z-" => "~", "z." => "…", "z/" => "・", "z," => "‥",
|
|
55
|
+
"z(" => "(", "z)" => ")", "z[" => "『", "z]" => "』",
|
|
56
|
+
"zh" => "←", "zj" => "↓", "zk" => "↑", "zl" => "→", "zL" => "⇒",
|
|
57
|
+
"z " => " ",
|
|
45
58
|
}
|
|
46
59
|
|
|
47
60
|
HIRAGANA_PREFIXES = HIRAGANA_TABLE.keys.flat_map { |s|
|
|
@@ -94,7 +107,13 @@ module Textbringer
|
|
|
94
107
|
(s.size - 1).times.map { |i| s[0, i + 1] }
|
|
95
108
|
}.uniq
|
|
96
109
|
|
|
97
|
-
|
|
110
|
+
STATUS_NAMES = {
|
|
111
|
+
hiragana: "かな",
|
|
112
|
+
katakana: "カナ",
|
|
113
|
+
hankaku_katakana: "半カナ",
|
|
114
|
+
zenkaku_ascii: "全英",
|
|
115
|
+
ascii: "SKK:"
|
|
116
|
+
}
|
|
98
117
|
|
|
99
118
|
DEFAULT_CURSOR_COLORS = {
|
|
100
119
|
hiragana: "pink",
|
|
@@ -117,6 +136,7 @@ module Textbringer
|
|
|
117
136
|
@marker_pos = nil
|
|
118
137
|
@okuriiari = nil
|
|
119
138
|
@okurinasi = nil
|
|
139
|
+
@skk_server_socket = nil
|
|
120
140
|
end
|
|
121
141
|
|
|
122
142
|
def toggle
|
|
@@ -125,22 +145,26 @@ module Textbringer
|
|
|
125
145
|
update_cursor_color
|
|
126
146
|
else
|
|
127
147
|
reset_cursor_color
|
|
148
|
+
close_skk_server
|
|
128
149
|
end
|
|
129
150
|
end
|
|
130
151
|
|
|
131
152
|
def disable
|
|
132
153
|
super
|
|
133
154
|
reset_cursor_color
|
|
155
|
+
close_skk_server
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def on_activate
|
|
159
|
+
update_cursor_color if @enabled
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def on_deactivate
|
|
163
|
+
reset_cursor_color if @enabled
|
|
134
164
|
end
|
|
135
165
|
|
|
136
166
|
def status
|
|
137
|
-
|
|
138
|
-
when :converting then "▽"
|
|
139
|
-
when :selecting then "▼"
|
|
140
|
-
else
|
|
141
|
-
{ hiragana: "あ", katakana: "ア", hankaku_katakana: "ア",
|
|
142
|
-
zenkaku_ascii: "A", ascii: "A" }[@mode]
|
|
143
|
-
end
|
|
167
|
+
STATUS_NAMES[@mode]
|
|
144
168
|
end
|
|
145
169
|
|
|
146
170
|
def handle_event(event)
|
|
@@ -198,7 +222,7 @@ module Textbringer
|
|
|
198
222
|
process_romaji(event)
|
|
199
223
|
end
|
|
200
224
|
when "l"
|
|
201
|
-
if [:hiragana, :katakana, :hankaku_katakana].include?(@mode)
|
|
225
|
+
if @roman_buffer != "z" && [:hiragana, :katakana, :hankaku_katakana].include?(@mode)
|
|
202
226
|
@roman_buffer = +""
|
|
203
227
|
@mode = :ascii
|
|
204
228
|
Window.redisplay
|
|
@@ -208,7 +232,7 @@ module Textbringer
|
|
|
208
232
|
process_romaji(event)
|
|
209
233
|
end
|
|
210
234
|
when "L"
|
|
211
|
-
if [:hiragana, :katakana, :hankaku_katakana].include?(@mode)
|
|
235
|
+
if @roman_buffer != "z" && [:hiragana, :katakana, :hankaku_katakana].include?(@mode)
|
|
212
236
|
@roman_buffer = +""
|
|
213
237
|
@mode = :zenkaku_ascii
|
|
214
238
|
Window.redisplay
|
|
@@ -386,8 +410,8 @@ module Textbringer
|
|
|
386
410
|
if kana
|
|
387
411
|
@roman_buffer = +""
|
|
388
412
|
if @okuri_roman
|
|
389
|
-
# Completing okurigana
|
|
390
|
-
@okuri_kana = kana
|
|
413
|
+
# Completing okurigana (accumulate in case a vowel kana was already prepended)
|
|
414
|
+
@okuri_kana = (@okuri_kana || "") + kana
|
|
391
415
|
with_target_buffer do |buffer|
|
|
392
416
|
buffer.insert(kana)
|
|
393
417
|
end
|
|
@@ -426,7 +450,14 @@ module Textbringer
|
|
|
426
450
|
first_char = @roman_buffer[0]
|
|
427
451
|
last_char = @roman_buffer[-1]
|
|
428
452
|
@roman_buffer = +""
|
|
429
|
-
|
|
453
|
+
if @okuri_roman && (kana = HIRAGANA_TABLE[first_char])
|
|
454
|
+
# Vowel starts okurigana: accumulate kana and continue buffering the rest
|
|
455
|
+
@okuri_kana = (@okuri_kana || "") + kana
|
|
456
|
+
with_target_buffer { |b| b.insert(kana) }
|
|
457
|
+
Window.redisplay
|
|
458
|
+
else
|
|
459
|
+
append_yomi_kana(first_char)
|
|
460
|
+
end
|
|
430
461
|
process_converting_romaji(last_char)
|
|
431
462
|
end
|
|
432
463
|
|
|
@@ -454,9 +485,20 @@ module Textbringer
|
|
|
454
485
|
nil
|
|
455
486
|
end
|
|
456
487
|
|
|
457
|
-
def start_okurigana(
|
|
458
|
-
@okuri_roman =
|
|
459
|
-
|
|
488
|
+
def start_okurigana(c)
|
|
489
|
+
@okuri_roman = c.dup
|
|
490
|
+
kana = HIRAGANA_TABLE[c]
|
|
491
|
+
if kana
|
|
492
|
+
# Vowel okurigana: insert the kana immediately and record it in @okuri_kana.
|
|
493
|
+
# (A vowel is never a prefix of a longer romaji sequence, so it's always complete.)
|
|
494
|
+
@okuri_kana = kana
|
|
495
|
+
@roman_buffer = +""
|
|
496
|
+
with_target_buffer { |b| b.insert(kana) }
|
|
497
|
+
Window.redisplay
|
|
498
|
+
start_selecting
|
|
499
|
+
else
|
|
500
|
+
@roman_buffer = c.dup
|
|
501
|
+
end
|
|
460
502
|
end
|
|
461
503
|
|
|
462
504
|
def cancel_converting
|
|
@@ -489,16 +531,19 @@ module Textbringer
|
|
|
489
531
|
end
|
|
490
532
|
|
|
491
533
|
def start_selecting
|
|
492
|
-
ensure_dictionary_loaded
|
|
493
|
-
|
|
494
534
|
lookup_key = if @okuri_roman
|
|
495
535
|
@yomi + @okuri_roman
|
|
496
536
|
else
|
|
497
537
|
@yomi
|
|
498
538
|
end
|
|
499
539
|
|
|
500
|
-
|
|
501
|
-
|
|
540
|
+
candidates = if CONFIG[:skk_server_host]
|
|
541
|
+
skk_server_lookup(lookup_key)
|
|
542
|
+
else
|
|
543
|
+
ensure_dictionary_loaded
|
|
544
|
+
dict = @okuri_roman ? @okuriiari : @okurinasi
|
|
545
|
+
dict[lookup_key]
|
|
546
|
+
end
|
|
502
547
|
|
|
503
548
|
if candidates.nil? || candidates.empty?
|
|
504
549
|
message("No conversion: #{@yomi}")
|
|
@@ -569,7 +614,7 @@ module Textbringer
|
|
|
569
614
|
def ensure_dictionary_loaded
|
|
570
615
|
return if @okuriiari
|
|
571
616
|
|
|
572
|
-
path = CONFIG[:
|
|
617
|
+
path = CONFIG[:skk_dictionary_path]
|
|
573
618
|
@okuriiari = {}
|
|
574
619
|
@okurinasi = {}
|
|
575
620
|
section = :okuriiari
|
|
@@ -747,5 +792,70 @@ module Textbringer
|
|
|
747
792
|
STDOUT.write("\e]112\a")
|
|
748
793
|
STDOUT.flush
|
|
749
794
|
end
|
|
795
|
+
|
|
796
|
+
SKK_SERVER_TIMEOUT = 3 # seconds
|
|
797
|
+
|
|
798
|
+
def close_skk_server
|
|
799
|
+
return unless @skk_server_socket
|
|
800
|
+
begin
|
|
801
|
+
@skk_server_socket.write("0\n")
|
|
802
|
+
rescue IOError, Errno::EPIPE
|
|
803
|
+
end
|
|
804
|
+
@skk_server_socket.close rescue nil
|
|
805
|
+
@skk_server_socket = nil
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
def skk_server_connect
|
|
809
|
+
return true if @skk_server_socket && !@skk_server_socket.closed?
|
|
810
|
+
host = CONFIG[:skk_server_host]
|
|
811
|
+
return false unless host
|
|
812
|
+
port = CONFIG[:skk_server_port] || 1178
|
|
813
|
+
Timeout.timeout(SKK_SERVER_TIMEOUT) do
|
|
814
|
+
@skk_server_socket = TCPSocket.new(host, port)
|
|
815
|
+
end
|
|
816
|
+
true
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
def skk_server_lookup(lookup_key)
|
|
820
|
+
skk_server_connect
|
|
821
|
+
begin
|
|
822
|
+
Timeout.timeout(SKK_SERVER_TIMEOUT) do
|
|
823
|
+
@skk_server_socket.write("1#{lookup_key} \n".encode("EUC-JP"))
|
|
824
|
+
response = @skk_server_socket.gets("\n")
|
|
825
|
+
return nil unless response
|
|
826
|
+
response = response.encode("UTF-8", "EUC-JP", invalid: :replace, undef: :replace).chomp
|
|
827
|
+
return nil unless response.start_with?("1/")
|
|
828
|
+
body = response[2..]
|
|
829
|
+
candidates = body.split("/").map { |c| c.split(";").first&.strip }.compact.reject(&:empty?)
|
|
830
|
+
candidates.empty? ? nil : candidates
|
|
831
|
+
end
|
|
832
|
+
rescue Timeout::Error, IOError, Errno::EPIPE, Errno::ECONNRESET
|
|
833
|
+
@skk_server_socket.close rescue nil
|
|
834
|
+
@skk_server_socket = nil
|
|
835
|
+
nil
|
|
836
|
+
end
|
|
837
|
+
end
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
SKK_DICTIONARY_URL = "https://github.com/skk-dev/dict/raw/090619ac57ef230a0506c191b569fc8c82b1025b/SKK-JISYO.L"
|
|
841
|
+
|
|
842
|
+
module Commands
|
|
843
|
+
define_command(:skk_download_dictionary, doc: "Download SKK dictionary") do
|
|
844
|
+
path = CONFIG[:skk_dictionary_path]
|
|
845
|
+
if File.exist?(path) && !yes_or_no?("#{path} already exists. Overwrite it?")
|
|
846
|
+
return
|
|
847
|
+
end
|
|
848
|
+
background do
|
|
849
|
+
URI.open(SKK_DICTIONARY_URL) do |f|
|
|
850
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
851
|
+
File.open(path, "wb") do |out|
|
|
852
|
+
IO.copy_stream(f, out)
|
|
853
|
+
end
|
|
854
|
+
end
|
|
855
|
+
foreground do
|
|
856
|
+
message("Downloaded to #{path}")
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
end
|
|
750
860
|
end
|
|
751
861
|
end
|
data/lib/textbringer/keymap.rb
CHANGED
|
@@ -177,6 +177,7 @@ module Textbringer
|
|
|
177
177
|
GLOBAL_MAP.define_key("\C-z", :suspend_textbringer)
|
|
178
178
|
GLOBAL_MAP.define_key("\C-x\C-f", :find_file)
|
|
179
179
|
GLOBAL_MAP.define_key("\C-x\C-r", :find_file_read_only)
|
|
180
|
+
GLOBAL_MAP.define_key("\C-xd", :dired)
|
|
180
181
|
GLOBAL_MAP.define_key("\C-x\C-v", :find_alternate_file)
|
|
181
182
|
GLOBAL_MAP.define_key("\C-xb", :switch_to_buffer)
|
|
182
183
|
GLOBAL_MAP.define_key("\C-x\C-b", :list_buffers)
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
module Textbringer
|
|
2
|
+
class DiredMode < Mode
|
|
3
|
+
define_keymap :DIRED_MODE_MAP
|
|
4
|
+
DIRED_MODE_MAP.define_key("n", :dired_next_line_command)
|
|
5
|
+
DIRED_MODE_MAP.define_key(" ", :dired_next_line_command)
|
|
6
|
+
DIRED_MODE_MAP.define_key("\C-n", :dired_next_line_command)
|
|
7
|
+
DIRED_MODE_MAP.define_key("p", :dired_previous_line_command)
|
|
8
|
+
DIRED_MODE_MAP.define_key("\C-p", :dired_previous_line_command)
|
|
9
|
+
DIRED_MODE_MAP.define_key("^", :dired_up_directory_command)
|
|
10
|
+
DIRED_MODE_MAP.define_key("\C-m", :dired_find_file_command)
|
|
11
|
+
DIRED_MODE_MAP.define_key("f", :dired_find_file_command)
|
|
12
|
+
DIRED_MODE_MAP.define_key("o", :dired_find_file_other_window_command)
|
|
13
|
+
DIRED_MODE_MAP.define_key("d", :dired_flag_file_deletion_command)
|
|
14
|
+
DIRED_MODE_MAP.define_key("u", :dired_unmark_command)
|
|
15
|
+
DIRED_MODE_MAP.define_key("U", :dired_unmark_all_command)
|
|
16
|
+
DIRED_MODE_MAP.define_key("x", :dired_do_flagged_delete_command)
|
|
17
|
+
DIRED_MODE_MAP.define_key("R", :dired_do_rename_command)
|
|
18
|
+
DIRED_MODE_MAP.define_key("C", :dired_do_copy_command)
|
|
19
|
+
DIRED_MODE_MAP.define_key("+", :dired_create_directory_command)
|
|
20
|
+
DIRED_MODE_MAP.define_key("g", :dired_revert_command)
|
|
21
|
+
DIRED_MODE_MAP.define_key("q", :bury_buffer)
|
|
22
|
+
|
|
23
|
+
# Deletion-flagged lines
|
|
24
|
+
define_syntax :dired_flagged, /^D .+$/
|
|
25
|
+
# Symlinks
|
|
26
|
+
define_syntax :dired_symlink, /^[ D] \S+\s+\d+\s+[\d-]+ [\d:]+ .+ -> .+$/
|
|
27
|
+
# Directories
|
|
28
|
+
define_syntax :dired_directory, /^[ D] d\S+\s+\d+\s+[\d-]+ [\d:]+ .+\/$/
|
|
29
|
+
# Executables
|
|
30
|
+
define_syntax :dired_executable, /^[ D] -[r-][w-]x.+$/
|
|
31
|
+
|
|
32
|
+
PERM_BITS = [
|
|
33
|
+
["r", 0400], ["w", 0200], ["x", 0100],
|
|
34
|
+
["r", 0040], ["w", 0020], ["x", 0010],
|
|
35
|
+
["r", 0004], ["w", 0002], ["x", 0001]
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
def self.format_permissions(stat)
|
|
39
|
+
type = if stat.directory? then "d"
|
|
40
|
+
elsif stat.symlink? then "l"
|
|
41
|
+
elsif stat.pipe? then "p"
|
|
42
|
+
elsif stat.socket? then "s"
|
|
43
|
+
elsif stat.chardev? then "c"
|
|
44
|
+
elsif stat.blockdev? then "b"
|
|
45
|
+
else "-"
|
|
46
|
+
end
|
|
47
|
+
type + PERM_BITS.map { |ch, mask| stat.mode & mask != 0 ? ch : "-" }.join
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.generate_listing(dir)
|
|
51
|
+
entries = []
|
|
52
|
+
Dir.foreach(dir) do |name|
|
|
53
|
+
path = File.join(dir, name)
|
|
54
|
+
begin
|
|
55
|
+
stat = File.lstat(path)
|
|
56
|
+
perms = format_permissions(stat)
|
|
57
|
+
size = stat.size
|
|
58
|
+
mtime = stat.mtime.strftime("%Y-%m-%d %H:%M")
|
|
59
|
+
if stat.symlink?
|
|
60
|
+
begin
|
|
61
|
+
target = File.readlink(path)
|
|
62
|
+
rescue SystemCallError
|
|
63
|
+
target = "?"
|
|
64
|
+
end
|
|
65
|
+
display = "#{name} -> #{target}"
|
|
66
|
+
elsif stat.directory?
|
|
67
|
+
display = "#{name}/"
|
|
68
|
+
else
|
|
69
|
+
display = name
|
|
70
|
+
end
|
|
71
|
+
entries << {
|
|
72
|
+
name: name,
|
|
73
|
+
display: display,
|
|
74
|
+
perms: perms,
|
|
75
|
+
size: size,
|
|
76
|
+
mtime: mtime,
|
|
77
|
+
directory: stat.directory?
|
|
78
|
+
}
|
|
79
|
+
rescue SystemCallError => e
|
|
80
|
+
entries << {
|
|
81
|
+
name: name,
|
|
82
|
+
display: name,
|
|
83
|
+
perms: "??????????",
|
|
84
|
+
size: 0,
|
|
85
|
+
mtime: "????-??-?? ??:??",
|
|
86
|
+
directory: false,
|
|
87
|
+
error: e.message
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
entries.sort_by! { |e| [e[:directory] ? 0 : 1, e[:name].downcase] }
|
|
93
|
+
|
|
94
|
+
lines = [" #{dir}:\n"]
|
|
95
|
+
entries.each do |e|
|
|
96
|
+
line = " #{e[:perms]} #{e[:size].to_s.rjust(8)} #{e[:mtime]} #{e[:display]}\n"
|
|
97
|
+
lines << line
|
|
98
|
+
end
|
|
99
|
+
lines.join
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def initialize(buffer)
|
|
103
|
+
super(buffer)
|
|
104
|
+
buffer.keymap = DIRED_MODE_MAP
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
define_local_command(:dired_move_to_filename,
|
|
108
|
+
doc: "Move point to the filename on the current line.") do
|
|
109
|
+
@buffer.beginning_of_line
|
|
110
|
+
if @buffer.looking_at?(/^[D ] \S+\s+\d+\s+[\d-]+\s+[\d:]+\s+/)
|
|
111
|
+
@buffer.forward_char(@buffer.match_string(0).length)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
define_local_command(:dired_next_line, doc: "Move to next file line.") do
|
|
116
|
+
@buffer.next_line
|
|
117
|
+
dired_move_to_filename
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
define_local_command(:dired_previous_line, doc: "Move to previous file line.") do
|
|
121
|
+
first_file_line = @buffer.save_excursion {
|
|
122
|
+
@buffer.beginning_of_buffer
|
|
123
|
+
@buffer.current_line
|
|
124
|
+
}
|
|
125
|
+
if @buffer.current_line > first_file_line
|
|
126
|
+
@buffer.previous_line
|
|
127
|
+
end
|
|
128
|
+
dired_move_to_filename
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
define_local_command(:dired_up_directory, doc: "Go up to parent directory.") do
|
|
132
|
+
dir = @buffer[:dired_directory]
|
|
133
|
+
parent = File.dirname(dir)
|
|
134
|
+
dired(parent)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
define_local_command(:dired_find_file, doc: "Visit file or directory at point.") do
|
|
138
|
+
name = current_file_name
|
|
139
|
+
return unless name
|
|
140
|
+
dir = @buffer[:dired_directory]
|
|
141
|
+
path = File.join(dir, name)
|
|
142
|
+
if File.directory?(path)
|
|
143
|
+
dired(path)
|
|
144
|
+
else
|
|
145
|
+
find_file(path)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
define_local_command(:dired_find_file_other_window,
|
|
150
|
+
doc: "Visit file at point in other window.") do
|
|
151
|
+
name = current_file_name
|
|
152
|
+
return unless name
|
|
153
|
+
dir = @buffer[:dired_directory]
|
|
154
|
+
path = File.join(dir, name)
|
|
155
|
+
if Window.list.size == 1
|
|
156
|
+
split_window
|
|
157
|
+
end
|
|
158
|
+
other_window
|
|
159
|
+
if File.directory?(path)
|
|
160
|
+
dired(path)
|
|
161
|
+
else
|
|
162
|
+
find_file(path)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
define_local_command(:dired_flag_file_deletion,
|
|
167
|
+
doc: "Flag file at point for deletion.") do
|
|
168
|
+
set_flag("D")
|
|
169
|
+
dired_next_line
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
define_local_command(:dired_unmark, doc: "Remove deletion flag from file at point.") do
|
|
173
|
+
set_flag(" ")
|
|
174
|
+
dired_next_line
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
define_local_command(:dired_unmark_all, doc: "Remove all deletion flags.") do
|
|
178
|
+
@buffer.save_excursion do
|
|
179
|
+
@buffer.beginning_of_buffer
|
|
180
|
+
while !@buffer.end_of_buffer?
|
|
181
|
+
set_flag(" ")
|
|
182
|
+
@buffer.next_line
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
define_local_command(:dired_do_flagged_delete,
|
|
188
|
+
doc: "Delete files flagged for deletion.") do
|
|
189
|
+
files = collect_flagged_files
|
|
190
|
+
return if files.empty?
|
|
191
|
+
list = files.map { |f| " #{f}" }.join("\n")
|
|
192
|
+
if yes_or_no?("Delete these files?")
|
|
193
|
+
files.each do |name|
|
|
194
|
+
next if name == "." || name == ".."
|
|
195
|
+
path = File.join(@buffer[:dired_directory], name)
|
|
196
|
+
begin
|
|
197
|
+
if File.directory?(path) && !File.symlink?(path)
|
|
198
|
+
FileUtils.rm_rf(path)
|
|
199
|
+
else
|
|
200
|
+
File.delete(path)
|
|
201
|
+
end
|
|
202
|
+
rescue SystemCallError => e
|
|
203
|
+
message("Error deleting #{name}: #{e.message}")
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
dired_revert
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
define_local_command(:dired_do_rename, doc: "Rename/move file at point.") do
|
|
211
|
+
name = current_file_name
|
|
212
|
+
return unless name
|
|
213
|
+
dir = @buffer[:dired_directory]
|
|
214
|
+
src = File.join(dir, name)
|
|
215
|
+
dest = read_file_name("Rename #{name} to: ", default: dir + "/")
|
|
216
|
+
dest = File.expand_path(dest, dir)
|
|
217
|
+
FileUtils.mv(src, dest)
|
|
218
|
+
dired_revert
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
define_local_command(:dired_do_copy, doc: "Copy file at point.") do
|
|
222
|
+
name = current_file_name
|
|
223
|
+
return unless name
|
|
224
|
+
dir = @buffer[:dired_directory]
|
|
225
|
+
src = File.join(dir, name)
|
|
226
|
+
dest = read_file_name("Copy #{name} to: ", default: dir + "/")
|
|
227
|
+
dest = File.expand_path(dest, dir)
|
|
228
|
+
FileUtils.cp_r(src, dest)
|
|
229
|
+
dired_revert
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
define_local_command(:dired_create_directory, doc: "Create a new directory.") do
|
|
233
|
+
dir = @buffer[:dired_directory]
|
|
234
|
+
name = read_from_minibuffer("Create directory: ", default: dir + "/")
|
|
235
|
+
name = File.expand_path(name, dir)
|
|
236
|
+
FileUtils.mkdir_p(name)
|
|
237
|
+
dired_revert
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
define_local_command(:dired_revert, doc: "Refresh directory listing.") do
|
|
241
|
+
dir = @buffer[:dired_directory]
|
|
242
|
+
saved_name = current_file_name
|
|
243
|
+
saved_line = @buffer.current_line
|
|
244
|
+
@buffer.read_only_edit do
|
|
245
|
+
@buffer.clear
|
|
246
|
+
@buffer.insert(DiredMode.generate_listing(dir))
|
|
247
|
+
@buffer.beginning_of_buffer
|
|
248
|
+
@buffer.forward_line
|
|
249
|
+
end
|
|
250
|
+
if saved_name
|
|
251
|
+
@buffer.beginning_of_buffer
|
|
252
|
+
found = false
|
|
253
|
+
while !@buffer.end_of_buffer?
|
|
254
|
+
if current_file_name == saved_name
|
|
255
|
+
found = true
|
|
256
|
+
break
|
|
257
|
+
end
|
|
258
|
+
@buffer.next_line
|
|
259
|
+
end
|
|
260
|
+
unless found
|
|
261
|
+
goto_line(saved_line)
|
|
262
|
+
@buffer.beginning_of_line
|
|
263
|
+
if @buffer.end_of_buffer? || current_file_name.nil?
|
|
264
|
+
@buffer.end_of_buffer
|
|
265
|
+
@buffer.previous_line while current_file_name.nil? && @buffer.current_line > 2
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
dired_move_to_filename
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
private
|
|
273
|
+
|
|
274
|
+
def current_file_name
|
|
275
|
+
@buffer.save_excursion do
|
|
276
|
+
@buffer.beginning_of_line
|
|
277
|
+
# Line format: " perms size date time display_name"
|
|
278
|
+
# or: "D perms size date time display_name"
|
|
279
|
+
if @buffer.looking_at?(/^[D ] (\S+)\s+\d+\s+[\d-]+\s+[\d:]+\s+(.+)$/)
|
|
280
|
+
perms = @buffer.match_string(1)
|
|
281
|
+
display = @buffer.match_string(2)
|
|
282
|
+
# Strip symlink target: "name -> target" -> "name" (only for symlinks)
|
|
283
|
+
display = display.sub(/ -> .+$/, "") if perms.start_with?("l")
|
|
284
|
+
# Strip trailing slash for directories: "name/" -> "name"
|
|
285
|
+
display = display.chomp("/")
|
|
286
|
+
display
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def set_flag(char)
|
|
292
|
+
@buffer.read_only_edit do
|
|
293
|
+
@buffer.save_excursion do
|
|
294
|
+
@buffer.beginning_of_line
|
|
295
|
+
if @buffer.looking_at?(/^[D ]/)
|
|
296
|
+
@buffer.delete_char(1)
|
|
297
|
+
@buffer.insert(char)
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def collect_flagged_files
|
|
304
|
+
files = []
|
|
305
|
+
@buffer.save_excursion do
|
|
306
|
+
@buffer.beginning_of_buffer
|
|
307
|
+
while !@buffer.end_of_buffer?
|
|
308
|
+
@buffer.beginning_of_line
|
|
309
|
+
if @buffer.looking_at?(/^D (\S+)\s+\d+\s+[\d-]+\s+[\d:]+\s+(.+)$/)
|
|
310
|
+
perms = @buffer.match_string(1)
|
|
311
|
+
display = @buffer.match_string(2)
|
|
312
|
+
display = display.sub(/ -> .+$/, "") if perms.start_with?("l")
|
|
313
|
+
display = display.chomp("/")
|
|
314
|
+
files << display
|
|
315
|
+
end
|
|
316
|
+
@buffer.next_line
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
files
|
|
320
|
+
end
|
|
321
|
+
end
|
|
322
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module Textbringer
|
|
2
|
+
class GamegridMode < Mode
|
|
3
|
+
@syntax_table = {}
|
|
4
|
+
|
|
5
|
+
def self.inherited(child)
|
|
6
|
+
super
|
|
7
|
+
child.instance_variable_set(:@syntax_table, {})
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
define_keymap :GAMEGRID_MODE_MAP
|
|
11
|
+
GAMEGRID_MODE_MAP.define_key("q", :gamegrid_quit_command)
|
|
12
|
+
|
|
13
|
+
def initialize(buffer)
|
|
14
|
+
super(buffer)
|
|
15
|
+
buffer.keymap = GAMEGRID_MODE_MAP
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
define_local_command(:gamegrid_init,
|
|
20
|
+
doc: "Initialize a gamegrid in the current buffer.") do |width, height, margin_left: 0|
|
|
21
|
+
grid = Gamegrid.new(width, height, margin_left: margin_left)
|
|
22
|
+
@buffer[:gamegrid] = grid
|
|
23
|
+
@buffer.read_only = true
|
|
24
|
+
@buffer[:highlight_override] = -> { grid.face_map }
|
|
25
|
+
grid
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
define_local_command(:gamegrid_refresh,
|
|
29
|
+
doc: "Refresh the gamegrid display.") do
|
|
30
|
+
grid = @buffer[:gamegrid]
|
|
31
|
+
return unless grid
|
|
32
|
+
@buffer.read_only_edit do
|
|
33
|
+
@buffer.clear
|
|
34
|
+
@buffer.insert(grid.render)
|
|
35
|
+
@buffer.beginning_of_buffer
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
define_local_command(:gamegrid_quit,
|
|
40
|
+
doc: "Quit the current game.") do
|
|
41
|
+
grid = @buffer[:gamegrid]
|
|
42
|
+
grid&.stop_timer
|
|
43
|
+
bury_buffer
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|