diff --git a/examples/client-cqchat.lua b/examples/client-cqchat.lua new file mode 100644 index 0000000..9f5c711 --- /dev/null +++ b/examples/client-cqchat.lua @@ -0,0 +1,182 @@ +#! /usr/bin/env lua +-- +-- client-cqchat.lua +-- Copyright (C) 2016 Adrian Perez +-- +-- 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[%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") + 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 ! No such room") + end + end + else + if current_room then + current_room:send_text(line) + else + print("\r ! Choose a room using '/room '") + 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 \n", arg[0])) + os.exit(1) +end + +local client = matrix.client(arg[1]) + :hook("logged-in", function (client) + print("\r * Logged in as " .. client.user_id) + end) + :hook("joined", function (client, room) + print("\r * Joined room " .. room.room_id) + room:hook("message", print_room_message) + end) + :hook("left", function (client, room) + print("\r * Left room " .. room.room_id) + end) + +tty:wrap(main, client, arg[2], arg[3])