1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
| #### libremiliacr
#### Copyright(C) 2020-2024 Remilia Scarlet <remilia@posteo.jp>
####
#### This program is free software: you can redistribute it and/or modify
#### it under the terms of the GNU General Public License as published
#### the Free Software Foundation, either version 3 of the License, or
#### (at your option) any later version.
####
#### This program is distributed in the hope that it will be useful,
#### but WITHOUT ANY WARRANTY; without even the implied warranty of
#### MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
#### GNU General Public License for more details.
####
#### You should have received a copy of the GNU General Public License
#### along with this program.If not, see<http:####www.gnu.org/licenses/.>
module RemiLib::Console
# The `RemiLib::Console::Terminology` module provides additional escape codes
# for the Terminology terminal emulator.
#
# [https://www.enlightenment.org/about-terminology](https://www.enlightenment.org/about-terminology)
module Terminology
extend self
# :nodoc:
QUERY = "\u{1B}[=c".to_slice
# :nodoc:
TERMINOLOGY_EXPECTED = "\u{1B}P!|7E7E5459\u{1B}\\".to_slice
# How `#showMedia` displays things.
enum DisplayMode
Centered
Filled
Stretched
end
private def withEscape(output, *, noEOS = false, &) : Nil
output.write_byte(0x1B) # #\Escape
output.write_byte(0x7D) # #\}
yield
unless noEOS
output.write_byte(0)
end
end
# Returns true if the program is connected running in a Terminology
# terminal, or false if it is not or it cannot be determined.
def runningTerminology?(*, output : IO::FileDescriptor = STDOUT, input : IO::FileDescriptor = STDIN) : Bool
return false unless LibC.tcgetattr(output.fd, out origAttrs) == 0
return false unless LibC.tcgetattr(output.fd, out newAttrs) == 0
result : String = ""
begin
# Turn off canonical (buffered) mode and echo
newAttrs.c_lflag &= ~(newAttrs.c_lflag | LibC::ICANON.value | LibC::ECHO.value)
# Minimum of number input read: 1 byte
newAttrs.c_cc[LibC::VMIN] = 1
{% begin %}
{% if LibC.has_constant?(:VTIME) %}
newAttrs.c_cc[LibC::VTIME] = 0
{% else %}
# Value retrieved from SBCL, which stores this as sb-posix:vtime.
# Probably not too portable.
newAttrs.c_cc[5] = 0
{% end %}
{% end %}
# Set the new terminal attributes
LibC.tcsetattr(output.fd, LibC::TCSANOW, pointerof(newAttrs))
# Send the query about the device attributes
output.write(QUERY)
output.flush
# Read input
result = String.build do |str|
TERMINOLOGY_EXPECTED.size.times do |_|
byte = input.read_byte || break
output.write_byte(byte)
str.write_byte(byte)
end
end
result.size == TERMINOLOGY_EXPECTED.size && result.to_slice == TERMINOLOGY_EXPECTED
ensure
# Restore old terminal attributes.
LibC.tcsetattr(output.fd, LibC::TCSANOW, pointerof(origAttrs))
end
end
private macro checkMediaDimensions(width, height)
raise "Maximum {{width}} is 512" if {{width}} > 512
raise "Maximum {{height}} is 512" if {{height}} > 512
raise "Minimum {{width}} is 1" if {{width}} <= 0
raise "Minimum {{height}} is 1" if {{height}} <= 0
end
# Displays a media file (or a URL pointing to media) in the terminal.
# `width` and `height` are in cells, not pixels.
def showMedia(filename : String|Path, width : UInt16, height : UInt16,
*, output : IO::FileDescriptor = STDOUT, dispMode : DisplayMode = DisplayMode::Centered) : Nil
raise File::NotFoundError.new("Cannot find #{filename}", file: filename) unless File.exists?(filename)
checkMediaDimensions(width, height)
lineStr = "#" * width
cmd = case dispMode
in DisplayMode::Centered then "ic"
in DisplayMode::Filled then "if"
in DisplayMode::Stretched then "is"
end
withEscape(output, noEOS: true) do
outstr = String.build do |str|
str << cmd << '#' << width << ';' << height << ';' << File.expand_path(filename) << '\0'
height.times do |i|
str << "\u{1B}}ib\0" << lineStr << "\u{1B}}ie\0\n"
end
end
output << outstr
end
end
# Displays a media file (or a URL pointing to media) thumbnail in the
# terminal. `width` and `height` are in cells, not pixels.
def showThumb(filename : String|Path, width : UInt16, height : UInt16,
*, output : IO::FileDescriptor = STDOUT, link : Bool|String = false) : Nil
checkMediaDimensions(width, height)
lineStr = "#" * width
withEscape(output, noEOS: true) do
outstr = String.build do |str|
str << "it#" << width << ';' << height << ';'
if link.is_a?(Bool) && link
str << File.expand_path(filename) << "\0\n" << File.expand_path(filename) << '\0'
elsif link.is_a?(String)
str << link << "\n" << File.expand_path(filename) << '\0'
else
str << File.expand_path(filename) << '\0'
end
height.times do |i|
str << "\u{1B}}ib\0" << lineStr << "\u{1B}}ie\0\n"
end
end
output << outstr
end
end
# Queries the grid and font size, then returns a tuple with four values:
#
# 1. The width of the terminal in characters
# 2. The height of the terminal in characters
# 3. The width of one character in pixels
# 4. The height of one character in pixels
#
# This returns `{0, 0, 0, 0}` if it cannot determine the sizes.
def queryGrid(*, output : IO::FileDescriptor = STDOUT, input : IO::FileDescriptor = STDIN) : Tuple(Int32, Int32, Int32, Int32)
withEscape(output) do
input.flush
output << "qs"
end
result : String = input.raw &.gets
return {0,0,0,0} unless result
nums = result.strip.split(';').map &.to_i32
raise "Unexpected result from terminal" unless nums.size == 4
{nums[0], nums[1], nums[2], nums[3]}
end
# Sets the background state on or off. If `makePermanent` is false (the
# default), then the change is a temporary one.
def setAlphaState(enable : Bool, *, output : IO::FileDescriptor = STDOUT, makePermanent : Bool = false) : Nil
withEscape(output) do
if enable
if makePermanent
output << "apon"
else
output << "aton"
end
else
if makePermanent
output << "apoff"
else
output << "atoff"
end
end
end
end
# Sets the background to the given media file. If `makePermanent` is false
# (the default), then the change is a temporary one.
def setBackground(filename : String|Path, *, output : IO::FileDescriptor = STDOUT,
makePermanent : Bool = false) : Nil
withEscape(output) do
if makePermanent
output << "bp" << File.expand_path(filename)
else
output << "bt" << File.expand_path(filename)
end
end
end
# Pops a media file (or a URL pointing to media) up in the terminal. If
# `enqueue` is true, then the popup is queued up by the terminal, otherwise
# it is immediately shown.
def popup(filename : String|Path, *, output : IO::FileDescriptor = STDOUT, enqueue : Bool = false) : Nil
withEscape(output) do
if enqueue
output << "pq" << File.expand_path(filename)
else
output << "pn" << File.expand_path(filename)
end
end
end
end
end
|