#### 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