-- This test suite tests the various screenshot related APIs. -- -- Credit: https://www.reddit.com/r/awesomewm/comments/i6nf7z/need_help_writing_a_color_picker_widget_using_lgi/ local awful = require("awful") local wibox = require("wibox") local spawn = require("awful.spawn") local gsurface = require("gears.surface") local gdebug = require("gears.debug") local lgi = require('lgi') local cairo = lgi.cairo local gdk = lgi.require('Gdk', '3.0') local capi = { root = _G.root } -- Dummy blue client for the client.content test -- the lua_executable portion may need to get ironed out. I need to specify 5.3 local lua_executable = os.getenv("LUA") if lua_executable == nil or lua_executable == "" then lua_executable = "lua5.3" end local client_dim = 250 local tiny_client_code_template = [[ local lgi = require('lgi') local Gdk = lgi.require('Gdk', '3.0') local Gtk = lgi.require('Gtk', '3.0') local class = 'client' window = Gtk.Window {default_width=%d, default_height=%d, title='title'} window:set_wmclass(class, class) window:override_background_color(0, Gdk.RGBA({red = 0, green = 0, blue = 1, alpha = 1})) window:show_all() Gtk:main{...} ]] local tiny_client = { lua_executable, "-e", string.format( tiny_client_code_template, client_dim, client_dim)} -- We need a directory to configure the screenshot library to use even through -- we never actually write a file. local fake_screenshot_dir = "/tmp" -- Split in the screen into 2 distict screens. screen[1]:split() -- Add a green wibox on screen 1 at (100, 100) wibox { bg = "#00ff00", visible = true, width = 100, height = 100, x = 100, y = 100, } local corners = {} -- Add red squares at 1 pixel away from each screen corners. for s in screen do -- Top left table.insert(corners, { x = s.geometry.x + 1, y = s.geometry.y + 1, }) -- Top right table.insert(corners, { x = s.geometry.x + s.geometry.width - 11, y = s.geometry.y + 1, }) -- Bottom left table.insert(corners, { x = s.geometry.x + 1, y = s.geometry.y + s.geometry.height - 11, }) -- Bottom right table.insert(corners, { x = s.geometry.x + s.geometry.width - 11, y = s.geometry.y + s.geometry.height - 11, }) end -- Make 10x10 wibox in each corner to eliminate any geometry errors for _, corner in ipairs(corners) do corner.bg = "#ff0000" corner.visible = true corner.width = 10 corner.height = 10 wibox(corner) end local function copy_to_image_surface(content, w, h) local sur = gsurface(content) local img = cairo.ImageSurface(cairo.Format.RGB24, w, h) local cr = cairo.Context(img) cr:set_source_surface(sur) cr:paint() img:flush() return img end local function get_pixel(img, x, y) local bytes = gdk.pixbuf_get_from_surface(img, x, y, 1, 1):get_pixels() return "#" .. bytes:gsub('.', function(c) return ('%02x'):format(c:byte()) end) end local snipper_success = nil local function snipper_cb(ss) local img = ss.surface snipper_success = img and get_pixel(img, 10, 10) == "#00ff00" end local steps = {} -- Check the whole root window with root.content() table.insert(steps, function() local root_width, root_height = root.size() local img = copy_to_image_surface(capi.root.content(), root_width, root_height) if get_pixel(img, 100, 100) ~= "#00ff00" then return end if get_pixel(img, 2, 2) ~= "#ff0000" then return end assert(get_pixel(img, 100, 100) == "#00ff00") assert(get_pixel(img, 199, 199) == "#00ff00") assert(get_pixel(img, 201, 201) ~= "#00ff00") assert(get_pixel(img, 2, 2) == "#ff0000") assert(get_pixel(img, root_width - 2, 2) == "#ff0000") assert(get_pixel(img, 2, root_height - 2) == "#ff0000") assert(get_pixel(img, root_width - 2, root_height - 2) == "#ff0000") return true end) -- Check screen.content table.insert(steps, function() for s in screen do local geo = s.geometry local img = copy_to_image_surface(s.content, geo.width, geo.height) assert(get_pixel(img, 4, 4) == "#ff0000") assert(get_pixel(img, geo.width - 4, 4) == "#ff0000") assert(get_pixel(img, 4, geo.height - 4) == "#ff0000") assert(get_pixel(img, geo.width - 4, geo.height - 4) == "#ff0000") --[[ assert(get_pixel(img, geo.x + 4, geo.y + 4) == "#ff0000") assert(get_pixel(img, geo.x + geo.width - 4, geo.y + 4) == "#ff0000") assert(get_pixel(img, geo.x + 4, geo.y + geo.height - 4) == "#ff0000") assert(get_pixel(img, geo.x + geo.width - 4, geo.y + geo.height - 4) == "#ff0000") --]] end -- Spawn for the client.content test assert(#client.get() == 0) spawn(tiny_client) return true end) -- Check client.content table.insert(steps, function() if #client.get() ~= 1 then return end local c = client.get()[1] local geo = c:geometry() local img = copy_to_image_surface(c.content, geo.width, geo.height) if get_pixel(img, math.floor(geo.width / 2), math.floor(geo.height / 2)) ~= "#0000ff" then return end -- Make sure the process finishes. Just `c:kill()` only -- closes the window. Adding some handlers to the GTK "app" -- created some unwanted side effects in the CI. awesome.kill(c.pid, 9) return true end) table.insert(steps, function() --Make sure client from last test is gone if #client.get() ~= 0 then return end local fake_screenshot_dir2 = string.gsub(fake_screenshot_dir, "/*$", "/", 1) local ss = awful.screenshot { directory = "/tmp" } local name_prfx = fake_screenshot_dir2 .. "Screenshot-" local f = string.find(ss.file_path, name_prfx) if f ~= 1 then error("Failed autogenerate filename: " .. ss.file_path .. " : " .. name_prfx) return false end name_prfx = fake_screenshot_dir2 .. "MyShot.png" ss.file_path = name_prfx if ss.file_path ~= name_prfx then error("Failed assign filename: " .. ss.file_path .. " : " .. name_prfx) return false end return true end) --Check the root window with awful.screenshot.root() method table.insert(steps, function() local root_width, root_height = root.size() local ss = awful.screenshot { directory = "/tmp" } ss:refresh() local img = ss.surface assert(img) assert(get_pixel(img, 100, 100) == "#00ff00") assert(get_pixel(img, 199, 199) == "#00ff00") assert(get_pixel(img, 201, 201) ~= "#00ff00") assert(get_pixel(img, 2, 2) == "#ff0000") assert(get_pixel(img, root_width - 2, 2) == "#ff0000") assert(get_pixel(img, 2, root_height - 2) == "#ff0000") assert(get_pixel(img, root_width - 2, root_height - 2) == "#ff0000") if ss.screen ~= nil or ss.client ~= nil then error("Returned non nil screen or client for root screenshot") return false end return true end) -- Check the awful.screenshot.screen() method table.insert(steps, function() for s in screen do local geo = s.geometry local ss = awful.screenshot {screen = s, directory = "/tmp" } ss:refresh() local img = ss.surface assert(img) assert(get_pixel(img, 4, 4) == "#ff0000") assert(get_pixel(img, geo.width - 4, 4) == "#ff0000") assert(get_pixel(img, 4, geo.height - 4) == "#ff0000") assert(get_pixel(img, geo.width - 4, geo.height - 4) == "#ff0000") end -- Spawn for the client.content test assert(#client.get() == 0) spawn(tiny_client) return true end) -- Check the awful.screenshot.client() method table.insert(steps, function() if #client.get() ~= 1 then return end local c = client.get()[1] local geo = c:geometry() local ss = awful.screenshot {client = c, directory = "/tmp" } ss:refresh() local img = ss.surface assert(img) if get_pixel(img, math.floor(geo.width / 2), math.floor(geo.height / 2)) ~= "#0000ff" then return end -- Make sure the process finishes. Just `c:kill()` only -- closes the window. Adding some handlers to the GTK "app" -- created some unwanted side effects in the CI. awesome.kill(c.pid, 9) return true end) --Check the snipper toop with awful.screenshot.snipper() method table.insert(steps, function() --Make sure client from last test is gone if #client.get() ~= 0 then return end --Ensure mousegrabber is satisfied root.fake_input("button_press",1) return true end) table.insert(steps, function() root.fake_input("button_release",1) awesome.sync() return true end) table.insert(steps, function() local ss = awful.screenshot { interactive = true, directory = "/tmp" } ss:refresh() ss:connect_signal("snipping::success", snipper_cb) return true end) table.insert(steps, function() mouse.coords {x = 110, y = 110} return true end) table.insert(steps, function() root.fake_input("button_press",1) return true end) table.insert(steps, function() mouse.coords {x = 190, y = 190} awesome.sync() return true end) table.insert(steps, function() root.fake_input("button_release",1) return true end) table.insert(steps, function() if snipper_success == nil then return end return snipper_success end) --Check the snipper collapse and cancel table.insert(steps, function() --Make sure client from last test is gone if #client.get() ~= 0 then return end --Ensure mousegrabber is satisfied root.fake_input("button_press",1) return true end) table.insert(steps, function() root.fake_input("button_release",1) awesome.sync() return true end) table.insert(steps, function() local ss = awful.screenshot { interactive = true, directory = "/tmp" } ss:connect_signal("snipping::success", snipper_cb) return true end) table.insert(steps, function() mouse.coords {x = 110, y = 110} return true end) table.insert(steps, function() root.fake_input("button_press",1) return true end) table.insert(steps, function() root.fake_input("button_release",1) return true end) table.insert(steps, function() mouse.coords {x = 150, y = 150} awesome.sync() return true end) table.insert(steps, function() --Cause a rectangle collapse mouse.coords {x = 150, y = 110} awesome.sync() return true end) table.insert(steps, function() --Cancel snipper tool root.fake_input("button_press",3) return true end) table.insert(steps, function() root.fake_input("button_release",3) awesome.sync() return true end) table.insert(steps, function() local ss = awful.screenshot { geometry = {x = 100, y = 100, width = 100, height = 100} } ss:refresh() local img = ss.surface return get_pixel(img, 10, 10) == "#00ff00" end) local escape_works, enter_works = false, false table.insert(steps, function() local ss = awful.screenshot { interactive = true } ss:connect_signal("snipping::start", function() ss:connect_signal("snipping::cancelled", function() enter_works = true end) end) ss:refresh() return true end) table.insert(steps, function() if not enter_works then root.fake_input("key_press","Return") root.fake_input("key_release","Return") return end local ss = awful.screenshot { interactive = true } ss:connect_signal("snipping::cancelled", function() escape_works = true end) ss:refresh() return true end) -- Test auto-save table.insert(steps, function() if not escape_works then root.fake_input("key_press","Escape") root.fake_input("key_release","Escape") return end local called = false local save = awful.screenshot.save awful.screenshot.save = function() called = true end local ss = awful.screenshot { auto_save_delay = 0 } ss:connect_signal("snipping::cancelled", function() escape_works = true end) awful.screenshot.save = save assert(called) return true end) local timer_start, timer_tick, timer_timeout, saved = false, false, false, false -- Test delayed auto-save table.insert(steps, function() local ss = awful.screenshot { auto_save_tick_duration = 0.05 } ss:connect_signal("timer::started", function() timer_start = true end) ss:connect_signal("timer::tick", function() timer_tick = true end) ss:connect_signal("timer::timeout", function() timer_timeout = true end) ss:connect_signal("file::saved", function() saved = true end) ss.auto_save_delay = 1 return true end) table.insert(steps, function() if not (timer_start and timer_tick and timer_timeout and saved) then return end return true end) table.insert(steps, function() local ss = awful.screenshot { auto_save_delay = 10 } -- Reach into some `if`. assert(not ss.selected_geometry) assert(not ss.surface) assert(#ss.surfaces == 0) ss.auto_save_delay = 0 ss.minimum_size = 2 ss.minimum_size = {width = 1, height = 1} ss.minimum_size = nil assert(ss.surface) assert(next(ss.surfaces) ~= nil) local count = 0 local err = gdebug.print_error function gdebug.print_error() count = count + 1 end -- Cause some validation failures. ss.prefix = "//////" assert(count == 1) ss.directory = "/tmp/////" ss.directory = "/tmp" ss.directory = "/root/" assert(count == 2) gdebug.print_error = err return true end) require("_runner").run_steps(steps)