diff options
Diffstat (limited to 'awesome/lib/awful/rules.lua')
-rw-r--r-- | awesome/lib/awful/rules.lua | 545 |
1 files changed, 545 insertions, 0 deletions
diff --git a/awesome/lib/awful/rules.lua b/awesome/lib/awful/rules.lua new file mode 100644 index 0000000..dc44e00 --- /dev/null +++ b/awesome/lib/awful/rules.lua @@ -0,0 +1,545 @@ +--------------------------------------------------------------------------- +--- Apply rules to clients at startup. +-- +-- @author Julien Danjou <julien@danjou.info> +-- @copyright 2009 Julien Danjou +-- @module awful.rules +--------------------------------------------------------------------------- + +-- Grab environment we need +local client = client +local awesome = awesome +local screen = screen +local table = table +local type = type +local ipairs = ipairs +local pairs = pairs +local atag = require("awful.tag") +local util = require("awful.util") +local a_place = require("awful.placement") +local protected_call = require("gears.protected_call") + +local rules = {} + +--[[-- +This is the global rules table. + +You should fill this table with your rule and properties to apply. +For example, if you want to set xterm maximized at startup, you can add: + + { rule = { class = "xterm" }, + properties = { maximized_vertical = true, maximized_horizontal = true } } + +If you want to set mplayer floating at startup, you can add: + + { rule = { name = "MPlayer" }, + properties = { floating = true } } + +If you want to put Firefox on a specific tag at startup, you can add: + + { rule = { instance = "firefox" }, + properties = { tag = mytagobject } } + +Alternatively, you can specify the tag by name: + + { rule = { instance = "firefox" }, + properties = { tag = "3" } } + +If you want to put Thunderbird on a specific screen at startup, use: + + { rule = { instance = "Thunderbird" }, + properties = { screen = 1 } } + +Assuming that your X11 server supports the RandR extension, you can also specify +the screen by name: + + { rule = { instance = "Thunderbird" }, + properties = { screen = "VGA1" } } + +If you want to put Emacs on a specific tag at startup, and immediately switch +to that tag you can add: + + { rule = { class = "Emacs" }, + properties = { tag = mytagobject, switchtotag = true } } + +If you want to apply a custom callback to execute when a rule matched, +for example to pause playing music from mpd when you start dosbox, you +can add: + + { rule = { class = "dosbox" }, + callback = function(c) + awful.spawn('mpc pause') + end } + +Note that all "rule" entries need to match. If any of the entry does not +match, the rule won't be applied. + +If a client matches multiple rules, they are applied in the order they are +put in this global rules table. If the value of a rule is a string, then the +match function is used to determine if the client matches the rule. + +If the value of a property is a function, that function gets called and +function's return value is used for the property. + +To match multiple clients to a rule one need to use slightly different +syntax: + + { rule_any = { class = { "MPlayer", "Nitrogen" }, instance = { "xterm" } }, + properties = { floating = true } } + +To match multiple clients with an exception one can couple `rules.except` or +`rules.except_any` with the rules: + + { rule = { class = "Firefox" }, + except = { instance = "Navigator" }, + properties = {floating = true}, + }, + + { rule_any = { class = { "Pidgin", "Xchat" } }, + except_any = { role = { "conversation" } }, + properties = { tag = "1" } + } + + { rule = {}, + except_any = { class = { "Firefox", "Vim" } }, + properties = { floating = true } + } +]]-- +rules.rules = {} + +--- Check if a client matches a rule. +-- @client c The client. +-- @tab rule The rule to check. +-- @treturn bool True if it matches, false otherwise. +function rules.match(c, rule) + if not rule then return false end + for field, value in pairs(rule) do + if c[field] then + if type(c[field]) == "string" then + if not c[field]:match(value) and c[field] ~= value then + return false + end + elseif c[field] ~= value then + return false + end + else + return false + end + end + return true +end + +--- Check if a client matches any part of a rule. +-- @client c The client. +-- @tab rule The rule to check. +-- @treturn bool True if at least one rule is matched, false otherwise. +function rules.match_any(c, rule) + if not rule then return false end + for field, values in pairs(rule) do + if c[field] then + for _, value in ipairs(values) do + if c[field] == value then + return true + elseif type(c[field]) == "string" and c[field]:match(value) then + return true + end + end + end + end + return false +end + +--- Does a given rule entry match a client? +-- @client c The client. +-- @tab entry Rule entry (with keys `rule`, `rule_any`, `except` and/or +-- `except_any`). +-- @treturn bool +function rules.matches(c, entry) + return (rules.match(c, entry.rule) or rules.match_any(c, entry.rule_any)) and + (not rules.match(c, entry.except) and not rules.match_any(c, entry.except_any)) +end + +--- Get list of matching rules for a client. +-- @client c The client. +-- @tab _rules The rules to check. List with "rule", "rule_any", "except" and +-- "except_any" keys. +-- @treturn table The list of matched rules. +function rules.matching_rules(c, _rules) + local result = {} + for _, entry in ipairs(_rules) do + if (rules.matches(c, entry)) then + table.insert(result, entry) + end + end + return result +end + +--- Check if a client matches a given set of rules. +-- @client c The client. +-- @tab _rules The rules to check. List of tables with `rule`, `rule_any`, +-- `except` and `except_any` keys. +-- @treturn bool True if at least one rule is matched, false otherwise. +function rules.matches_list(c, _rules) + for _, entry in ipairs(_rules) do + if (rules.matches(c, entry)) then + return true + end + end + return false +end + +--- Apply awful.rules.rules to a client. +-- @client c The client. +function rules.apply(c) + + local props = {} + local callbacks = {} + + for _, entry in ipairs(rules.matching_rules(c, rules.rules)) do + if entry.properties then + for property, value in pairs(entry.properties) do + props[property] = value + end + end + if entry.callback then + table.insert(callbacks, entry.callback) + end + end + + rules.execute(c, props, callbacks) +end + +local function add_to_tag(c, t) + if not t then return end + + local tags = c:tags() + table.insert(tags, t) + c:tags(tags) +end + +--- Extra rules properties. +-- +-- These properties are used in the rules only and are not sent to the client +-- afterward. +-- +-- To add a new properties, just do: +-- +-- function awful.rules.extra_properties.my_new_property(c, value, props) +-- -- do something +-- end +-- +-- By default, the table has the following functions: +-- +-- * geometry +-- * switchtotag +-- +-- @tfield table awful.rules.extra_properties +rules.extra_properties = {} + +--- Extra high priority properties. +-- +-- Some properties, such as anything related to tags, geometry or focus, will +-- cause a race condition if set in the main property section. This is why +-- they have a section for them. +-- +-- To add a new properties, just do: +-- +-- function awful.rules.high_priority_properties.my_new_property(c, value, props) +-- -- do something +-- end +-- +-- By default, the table has the following functions: +-- +-- * tag +-- * new_tag +-- +-- @tfield table awful.rules.high_priority_properties +rules.high_priority_properties = {} + +--- Delayed properties. +-- Properties applied after all other categories. +-- @tfield table awful.rules.delayed_properties +rules.delayed_properties = {} + +local force_ignore = { + titlebars_enabled=true, focus=true, screen=true, x=true, + y=true, width=true, height=true, geometry=true,placement=true, + border_width=true,floating=true,size_hints_honor=true +} + +function rules.high_priority_properties.tag(c, value, props) + if value then + if type(value) == "string" then + value = atag.find_by_name(c.screen, value) + end + + -- In case the tag has been forced to another screen, move the client + if c.screen ~= value.screen then + c.screen = value.screen + props.screen = value.screen -- In case another rule query it + end + + c:tags{ value } + end +end + +function rules.delayed_properties.switchtotag(c, value) + if not value then return end + + local selected_tags = {} + + for _,v in ipairs(c.screen.selected_tags) do + selected_tags[v] = true + end + + local tags = c:tags() + + for _, t in ipairs(tags) do + t.selected = true + selected_tags[t] = nil + end + + for t in pairs(selected_tags) do + t.selected = false + end +end + +function rules.extra_properties.geometry(c, _, props) + local cur_geo = c:geometry() + + local new_geo = type(props.geometry) == "function" + and props.geometry(c, props) or props.geometry or {} + + for _, v in ipairs {"x", "y", "width", "height"} do + new_geo[v] = type(props[v]) == "function" and props[v](c, props) + or props[v] or new_geo[v] or cur_geo[v] + end + + c:geometry(new_geo) --TODO use request::geometry +end + +--- Create a new tag based on a rule. +-- @tparam client c The client +-- @tparam boolean|function|string value The value. +-- @tparam table props The properties. +-- @treturn tag The new tag +function rules.high_priority_properties.new_tag(c, value, props) + local ty = type(value) + local t = nil + + if ty == "boolean" then + -- Create a new tag named after the client class + t = atag.add(c.class or "N/A", {screen=c.screen, volatile=true}) + elseif ty == "string" then + -- Create a tag named after "value" + t = atag.add(value, {screen=c.screen, volatile=true}) + elseif ty == "table" then + -- Assume a table of tags properties. Set the right screen, but + -- avoid editing the original table + local values = value.screen and value or util.table.clone(value) + values.screen = values.screen or c.screen + + t = atag.add(value.name or c.class or "N/A", values) + + -- In case the tag has been forced to another screen, move the client + c.screen = t.screen + props.screen = t.screen -- In case another rule query it + else + assert(false) + end + + add_to_tag(c, t) + + return t +end + +function rules.extra_properties.placement(c, value) + -- Avoid problems + if awesome.startup and + (c.size_hints.user_position or c.size_hints.program_position) then + return + end + + local ty = type(value) + + local args = { + honor_workarea = true, + honor_padding = true + } + + if ty == "function" or (ty == "table" and + getmetatable(value) and getmetatable(value).__call + ) then + value(c, args) + elseif ty == "string" and a_place[value] then + a_place[value](c, args) + end +end + +function rules.extra_properties.tags(c, value, props) + local current = c:tags() + + local tags, s = {}, nil + + for _, t in ipairs(value) do + if type(t) == "string" then + t = atag.find_by_name(c.screen, t) + end + + if t and ((not s) or t.screen == s) then + table.insert(tags, t) + s = s or t.screen + end + end + + if s and s ~= c.screen then + c.screen = s + props.screen = s -- In case another rule query it + end + + if #current == 0 or (value[1] and value[1].screen ~= current[1].screen) then + c:tags(tags) + else + c:tags(util.table.merge(current, tags)) + end +end + +--- Apply properties and callbacks to a client. +-- @client c The client. +-- @tab props Properties to apply. +-- @tab[opt] callbacks Callbacks to apply. +function rules.execute(c, props, callbacks) + -- This has to be done first, as it will impact geometry related props. + if props.titlebars_enabled then + c:emit_signal("request::titlebars", "rules", {properties=props}) + end + + -- Border width will also cause geometry related properties to fail + if props.border_width then + c.border_width = type(props.border_width) == "function" and + props.border_width(c, props) or props.border_width + end + + -- Size hints will be re-applied when setting width/height unless it is + -- disabled first + if props.size_hints_honor ~= nil then + c.size_hints_honor = type(props.size_hints_honor) == "function" and props.size_hints_honor(c,props) + or props.size_hints_honor + end + + -- Geometry will only work if floating is true, otherwise the "saved" + -- geometry will be restored. + if props.floating ~= nil then + c.floating = type(props.floating) == "function" and props.floating(c,props) + or props.floating + end + + -- Before requesting a tag, make sure the screen is right + if props.screen then + c.screen = type(props.screen) == "function" and screen[props.screen(c,props)] + or screen[props.screen] + end + + -- Some properties need to be handled first. For example, many properties + -- depend that the client is tagged, this isn't yet the case. + for prop, handler in pairs(rules.high_priority_properties) do + local value = props[prop] + + if value ~= nil then + if type(value) == "function" then + value = value(c, props) + end + + handler(c, value, props) + end + + end + + -- By default, rc.lua use no_overlap+no_offscreen placement. This has to + -- be executed before x/y/width/height/geometry as it would otherwise + -- always override the user specified position with the default rule. + if props.placement then + -- It may be a function, so this one doesn't execute it like others + rules.extra_properties.placement(c, props.placement, props) + end + + -- Make sure the tag is selected before the main rules are called. + -- Otherwise properties like "urgent" or "focus" may fail because they + -- will be overiden by various callbacks. + -- Previously, this was done in a second client.manage callback, but caused + -- a race condition where the order the require() would change the output. + c:emit_signal("request::tag", nil, {reason="rules"}) + + -- By default, rc.lua use no_overlap+no_offscreen placement. This has to + -- be executed before x/y/width/height/geometry as it would otherwise + -- always override the user specified position with the default rule. + if props.placement then + -- It may be a function, so this one doesn't execute it like others + rules.extra_properties.placement(c, props.placement, props) + end + + -- Now that the tags and screen are set, handle the geometry + if props.height or props.width or props.x or props.y or props.geometry then + rules.extra_properties.geometry(c, nil, props) + end + + -- As most race conditions should now have been avoided, apply the remaining + -- properties. + for property, value in pairs(props) do + if property ~= "focus" and type(value) == "function" then + value = value(c, props) + end + + local ignore = rules.high_priority_properties[property] or + rules.delayed_properties[property] or force_ignore[property] + + if not ignore then + if rules.extra_properties[property] then + rules.extra_properties[property](c, value, props) + elseif type(c[property]) == "function" then + c[property](c, value) + else + c[property] = value + end + end + end + + -- Apply all callbacks. + if callbacks then + for _, callback in pairs(callbacks) do + protected_call(callback, c) + end + end + + -- Apply the delayed properties + for prop, handler in pairs(rules.delayed_properties) do + if not force_ignore[prop] then + local value = props[prop] + + if value ~= nil then + if type(value) == "function" then + value = value(c, props) + end + + handler(c, value, props) + end + end + end + + -- Do this at last so we do not erase things done by the focus signal. + if props.focus and (type(props.focus) ~= "function" or props.focus(c)) then + c:emit_signal('request::activate', "rules", {raise=true}) + end +end + +function rules.completed_with_payload_callback(c, props, callbacks) + rules.execute(c, props, callbacks) +end + +client.connect_signal("spawn::completed_with_payload", rules.completed_with_payload_callback) + +client.connect_signal("manage", rules.apply) + +return rules + +-- vim: filetype=lua:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:textwidth=80 |