Add (crappy) but functional interactive cqueues-based chat client
This commit is contained in:
182
examples/client-cqchat.lua
Normal file
182
examples/client-cqchat.lua
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
#! /usr/bin/env lua
|
||||||
|
--
|
||||||
|
-- client-cqchat.lua
|
||||||
|
-- Copyright (C) 2016 Adrian Perez <aperez@igalia.com>
|
||||||
|
--
|
||||||
|
-- Distributed under terms of the MIT license.
|
||||||
|
--
|
||||||
|
|
||||||
|
local cqueues = require "cqueues"
|
||||||
|
local posix = require "posix"
|
||||||
|
local bit = require "bit32"
|
||||||
|
local matrix = require "matrix"
|
||||||
|
|
||||||
|
local function xpcall_traceback(errmsg)
|
||||||
|
local tb = debug.traceback(nil, nil, 2)
|
||||||
|
return errmsg and (errmsg .. "\n" .. tb) or tb
|
||||||
|
end
|
||||||
|
|
||||||
|
--
|
||||||
|
-- Wraps the controlling terminal into an object which can be polled
|
||||||
|
-- with cqueues.poll() and uses O_NONBLOCK for input.
|
||||||
|
--
|
||||||
|
local tty = {
|
||||||
|
file = (function ()
|
||||||
|
local f = io.open(posix.ctermid(), "a+")
|
||||||
|
local fd = posix.fileno(f)
|
||||||
|
local flags = posix.fcntl(fd, posix.F_GETFL, 0)
|
||||||
|
flags = bit.bor(flags, assert(posix.O_NONBLOCK))
|
||||||
|
if posix.O_CLOEXEC then
|
||||||
|
flags = bit.bor(flags, posix.O_CLOEXEC)
|
||||||
|
else
|
||||||
|
io.stderr:write("no O_CLOEXEC, the TTY file descriptor might leak")
|
||||||
|
io.stderr:flush()
|
||||||
|
end
|
||||||
|
if posix.fcntl(fd, posix.F_SETFL, flags) ~= 0 then
|
||||||
|
error("cannot set O_NONBLOCK/O_CLOEXEC: " .. posix.errno())
|
||||||
|
end
|
||||||
|
return f
|
||||||
|
end)(),
|
||||||
|
|
||||||
|
-- Functions expected by cqueues
|
||||||
|
pollfd = function (self) return posix.fileno(self.file) end,
|
||||||
|
events = function () return "r" end, -- Read only
|
||||||
|
timeout = function () return nil end, -- Set in the call to cqueues.poll()
|
||||||
|
|
||||||
|
-- Wrappers for tcsetattr/tcgetattr
|
||||||
|
tcgetattr = function (self) return posix.tcgetattr(self:pollfd()) end,
|
||||||
|
tcsetattr = function (self, ...) return posix.tcsetattr(self:pollfd(), ...) end,
|
||||||
|
|
||||||
|
-- Obtain the terminal size using TIOCGWINSZ
|
||||||
|
_size_width = false,
|
||||||
|
_size_height = false,
|
||||||
|
|
||||||
|
size = function (self, force)
|
||||||
|
if force then
|
||||||
|
self._size_width = false
|
||||||
|
self._size_height = false
|
||||||
|
end
|
||||||
|
if self._size_width == false then
|
||||||
|
local p = io.popen("tput cols", "r")
|
||||||
|
self._size_width = p:read("*n")
|
||||||
|
p:close()
|
||||||
|
end
|
||||||
|
if self._size_height == false then
|
||||||
|
local p = io.popen("tput lines", "r")
|
||||||
|
self._size_height = p:read("*n")
|
||||||
|
p:close()
|
||||||
|
end
|
||||||
|
return self._size_width, self._size_height
|
||||||
|
end,
|
||||||
|
|
||||||
|
-- Saves terminal attributes, runs a function, and restores attributes
|
||||||
|
-- even if the function raises an error.
|
||||||
|
wrap = function (self, f, ...)
|
||||||
|
local saved_attr = self:tcgetattr()
|
||||||
|
local ok, err = xpcall(f, xpcall_traceback, self, ...)
|
||||||
|
self:tcsetattr(posix.TCSANOW, saved_attr)
|
||||||
|
if not ok then
|
||||||
|
error("tty:wrap: Error in wrapped function:\n" .. err)
|
||||||
|
end
|
||||||
|
return self
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
local command_pattern = "^%s*/(%a+)%s+(.*)$"
|
||||||
|
|
||||||
|
local function main(tty, client, username, password)
|
||||||
|
do
|
||||||
|
local a = tty:tcgetattr()
|
||||||
|
a.cc[posix.VMIN] = 1
|
||||||
|
a.cc[posix.VTIME] = 0
|
||||||
|
a.lflag = bit.band(a.lflag, bit.bnot(bit.bor(posix.ECHO, posix.ICANON)))
|
||||||
|
a.iflag = bit.band(a.iflag, bit.bnot(bit.bor(posix.IXON, posix.ISTRIP)))
|
||||||
|
a.cflag = bit.band(a.cflag, bit.bnot(bit.bor(posix.CSIZE, posix.PARENB)))
|
||||||
|
a.cflag = bit.bor(a.cflag, posix.CS8)
|
||||||
|
a.oflag = bit.band(a.oflag, bit.bnot(posix.OPOST))
|
||||||
|
if tty:tcsetattr(posix.TCSANOW, a) ~= 0 then
|
||||||
|
error("tcsetattr: " .. posix.errno())
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local cq = cqueues.new()
|
||||||
|
local ok, err, obj = cq:wrap(function ()
|
||||||
|
print(string.format("Terminal size: %dx%d", tty:size()))
|
||||||
|
client:login_with_password(username, password)
|
||||||
|
local clientqueue = cq:wrap(function ()
|
||||||
|
client:sync()
|
||||||
|
end)
|
||||||
|
|
||||||
|
local current_room
|
||||||
|
while true do
|
||||||
|
local line = ""
|
||||||
|
while true do
|
||||||
|
io.stdout:write(string.format("\r[K[%s] %s",
|
||||||
|
current_room and current_room.room_id or "*",
|
||||||
|
line))
|
||||||
|
io.stdout:flush()
|
||||||
|
|
||||||
|
local handle_tty = false
|
||||||
|
cqueues.poll(tty, 0.05)
|
||||||
|
|
||||||
|
local ch = tty.file:read(1)
|
||||||
|
if ch then
|
||||||
|
if ch == "\4" and #line == 0 then
|
||||||
|
print("\r[K")
|
||||||
|
cq:cancel()
|
||||||
|
return
|
||||||
|
elseif ch == "\127" then
|
||||||
|
line = line:sub(1, -2)
|
||||||
|
elseif ch == "\n" then
|
||||||
|
break
|
||||||
|
else
|
||||||
|
line = line .. ch
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local command, params = line:match(command_pattern)
|
||||||
|
if command then
|
||||||
|
if command == "room" then
|
||||||
|
if client.rooms[params] then
|
||||||
|
current_room = client.rooms[params]
|
||||||
|
else
|
||||||
|
print("\r[K ! No such room")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if current_room then
|
||||||
|
current_room:send_text(line)
|
||||||
|
else
|
||||||
|
print("\r[K ! Choose a room using '/room <room_id>'")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end):loop()
|
||||||
|
if not ok then
|
||||||
|
error(err)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function print_room_message(room, sender, message, event)
|
||||||
|
print(string.format("\rK[%s] <%s> %s", room.room_id, sender, message.body))
|
||||||
|
end
|
||||||
|
|
||||||
|
if #arg ~= 3 then
|
||||||
|
io.stderr:write(string.format("Usage: %s <homeserver> <username> <password>\n", arg[0]))
|
||||||
|
os.exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
local client = matrix.client(arg[1])
|
||||||
|
:hook("logged-in", function (client)
|
||||||
|
print("\r[K * Logged in as " .. client.user_id)
|
||||||
|
end)
|
||||||
|
:hook("joined", function (client, room)
|
||||||
|
print("\r[K * Joined room " .. room.room_id)
|
||||||
|
room:hook("message", print_room_message)
|
||||||
|
end)
|
||||||
|
:hook("left", function (client, room)
|
||||||
|
print("\r[K * Left room " .. room.room_id)
|
||||||
|
end)
|
||||||
|
|
||||||
|
tty:wrap(main, client, arg[2], arg[3])
|
||||||
Reference in New Issue
Block a user