#! /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])