-------------------------------------------
-- @author https://github.com/Kasper24
-- @copyright 2021-2025 Kasper24
-------------------------------------------
local lgi = require('lgi')
local Gtk = lgi.require('Gtk', '3.0')
local Gdk = lgi.require('Gdk', '3.0')
local Pango = lgi.Pango
local awful = require("awful")
local gtable = require("gears.table")
local gtimer = require("gears.timer")
local gcolor = require("gears.color")
local wibox = require("wibox")
local beautiful = require("beautiful")
local abs = math.abs
local ipairs = ipairs
local string = string
local capi = {
    awesome = awesome,
    root = root,
    tag = tag,
    client = client,
    mouse = mouse,
    mousegrabber = mousegrabber
}

local text_input = {
    mt = {}
}

local properties = {
    "unfocus_keys",
    "unfocus_on_root_clicked", "unfocus_on_client_clicked", "unfocus_on_client_focus",
    "unfocus_on_mouse_leave", "unfocus_on_tag_change",
    "focus_on_subject_mouse_enter", "unfocus_on_subject_mouse_leave",
    "click_timeout",
    "reset_on_unfocus",
    "text_color",
    "placeholder", "initial",
    "pattern", "obscure",
    "cursor_blink", "cursor_blink_rate","cursor_size", "cursor_bg",
    "selection_bg"
}

text_input.patterns = {
    numbers = "[%d.]*",
    numbers_one_decimal = "%d*%.?%d*",
    round_numbers = "[0-9]*",
    email = "%S+@%S+%.%S+",
    time = "%d%d?:%d%d:%d%d?|%d%d?:%d%d",
    date = "%d%d%d%d%-%d%d%-%d%d|%d%d?/%d%d?/%d%d%d%d|%d%d?%.%d%d?%.%d%d%d%d",
    phone = "%+?%d[%d%-%s]+%d",
    url = "https?://[%w-_%.]+%.[%w]+/?[%w-_%.?=%+]*",
    email = "[%w._%-%+]+@[%w._%-]+%.%w+",
    alphanumeric = "%w+",
    letters = "[a-zA-Z]+"
}

local function build_properties(prototype, prop_names)
    for _, prop in ipairs(prop_names) do
        if not prototype["set_" .. prop] then
            prototype["set_" .. prop] = function(self, value)
                if self._private[prop] ~= value then
                    self._private[prop] = value
                    self:emit_signal("widget::redraw_needed")
                    self:emit_signal("property::" .. prop, value)
                end
                return self
            end
        end
        if not prototype["get_" .. prop] then
            prototype["get_" .. prop] = function(self)
                return self._private[prop]
            end
        end
    end
end

local function has_value(tab, val)
    for _, value in ipairs(tab) do
        if val:lower():find(value:lower(), 1, true) then
            return true
        end
    end
    return false
end

local function is_word_char(c)
    if string.find(c, "[{[(,.:;_-+=@/ ]") then
        return false
    else
        return true
    end
end

local function cword_start(s, pos)
    local i = pos
    if i > 1 then
        i = i - 1
    end
    while i >= 1 and not is_word_char(s:sub(i, i)) do
        i = i - 1
    end
    while i >= 1 and is_word_char(s:sub(i, i)) do
        i = i - 1
    end
    if i <= #s then
        i = i + 1
    end
    return i
end

local function cword_end(s, pos)
    local i = pos
    while i <= #s and not is_word_char(s:sub(i, i)) do
        i = i + 1
    end
    while i <= #s and is_word_char(s:sub(i, i)) do
        i = i + 1
    end
    return i
end

local function set_mouse_cursor(cursor)
    capi.root.cursor(cursor)
    local wibox = capi.mouse.current_wibox
    if wibox then
        wibox.cursor = cursor
    end
end

local function single_double_triple_tap(self, args)
    local wp = self._private

    if wp.click_timer == nil then
        wp.click_timer = gtimer {
            timeout = wp.click_timeout,
            autostart = false,
            call_now = false,
            single_shot = true,
            callback = function()
                wp.click_count = 0
            end
        }
    end

    wp.click_timer:again()
    wp.click_count = wp.click_count + 1
    if wp.click_count == 1 then
        args.on_single_click()
    elseif wp.click_count == 2 then
        args.on_double_click()
    elseif wp.click_count == 3 then
        args.on_triple_click()
        wp.click_count = 0
    end
end

local function run_keygrabber(self)
    local wp = self._private
    wp.keygrabber = awful.keygrabber.run(function(modifiers, key, event)
        if event ~= "press" then
            self:emit_signal("key::release", modifiers, key, event)
            return
        end
        self:emit_signal("key::press", modifiers, key, event)

        -- Convert index array to hash table
        local mod = {}
        for _, v in ipairs(modifiers) do
            mod[v] = true
        end

        if mod.Control then
            if key == "a" then
                self:select_all()
            elseif key == "c" then
                self:copy()
            elseif key == "v" then
                self:paste()
            elseif key == "b" or key == "Left" then
                self:set_cursor_index_to_word_start()
            elseif key == "f" or key == "Right" then
                self:set_cursor_index_to_word_end()
            elseif key == "d" then
                self:delete_next_word()
            elseif key == "BackSpace" then
                self:delete_previous_word()
            end
        elseif mod.Shift and key:wlen() ~= 1 then
            if key =="Left" then
                self:decremeant_selection_end_index()
            elseif key == "Right" then
                self:increamant_selection_end_index()
            end
        else
            if has_value(wp.unfocus_keys, key) then
                self:unfocus()
            end

            if mod.Shift and key == "Insert" then
                self:paste()
            elseif key == "Home" then
                self:set_cursor_index(0)
            elseif key == "End" then
                self:set_cursor_index_to_end()
            elseif key == "BackSpace" then
                self:delete_text()
            elseif key == "Delete" then
                self:delete_text_after_cursor()
            elseif key == "Left" then
                self:decremeant_cursor_index()
            elseif key == "Right" then
                self:increamant_cursor_index()
            elseif key:wlen() == 1 then
                self:update_text(key)
            end
        end
    end)
end

function text_input:set_widget_template(widget_template)
    local wp = self._private

    wp.text_widget = widget_template:get_children_by_id("text_role")[1]
    wp.text_widget.forced_width = math.huge
    local text_draw = wp.text_widget.draw
    if self:get_initial() then
        self:replace_text(self:get_initial())
    end

    local placeholder_widget = widget_template:get_children_by_id("placeholder_role")
    if placeholder_widget then
        placeholder_widget = placeholder_widget[1]
    end

    function wp.text_widget:draw(context, cr, width, height)
        local _, logical_rect = self._private.layout:get_pixel_extents()

        -- Selection bg
        cr:set_source(gcolor.change_opacity(wp.selection_bg, wp.selection_opacity))
        cr:rectangle(
            wp.selection_start_x,
            logical_rect.y - 3,
            wp.selection_end_x - wp.selection_start_x,
            logical_rect.y + logical_rect.height + 6
        )
        cr:fill()

        -- Cursor
        cr:set_source(gcolor.change_opacity(wp.cursor_bg, wp.cursor_opacity))
        cr:set_line_width(wp.cursor_width)
        cr:move_to(wp.cursor_x, logical_rect.y - 3)
        cr:line_to(wp.cursor_x, logical_rect.y + logical_rect.height + 6)
        cr:stroke()

        cr:set_source(gcolor(wp.text_color))
        text_draw(self, context, cr, width, height)

        if self:get_text() == "" and placeholder_widget then
            placeholder_widget.visible = true
        elseif placeholder_widget then
            placeholder_widget.visible = false
        end
    end

    local function on_drag(_, lx, ly)
        lx, ly = wp.hierarchy:get_matrix_from_device():transform_point(lx, ly)
        if abs(lx - wp.press_pos.lx) > 2 or abs(ly - wp.press_pos.ly) > 2 then
            if self:get_mode() ~= "overwrite" then
                self:set_selection_start_index_from_x_y(wp.press_pos.lx, wp.press_pos.ly)
            end
            self:set_selection_end_index_from_x_y(lx, ly)
        end
    end

    wp.text_widget:connect_signal("button::press", function(_, lx, ly, button, mods, find_widgets_result)
		if gtable.hasitem(mods, "Mod4") or button ~= 1 then
			return
		end

        single_double_triple_tap(self, {
            on_single_click = function()
                self:focus()
                self:set_cursor_index_from_x_y(lx, ly)
            end,
            on_double_click = function()
                self:set_selection_to_word()
            end,
            on_triple_click = function()
                self:select_all()
            end
        })

        wp.press_pos = { lx = lx, ly = ly }
        wp.hierarchy = find_widgets_result.hierarchy
        find_widgets_result.drawable:connect_signal("mouse::move", on_drag)
    end)

    wp.text_widget:connect_signal("button::release", function(_, lx, ly, button, mods, find_widgets_result)
        if button == 1 then
            find_widgets_result.drawable:disconnect_signal("mouse::move", on_drag)
        end
    end)

    wp.text_widget:connect_signal("mouse::enter", function()
        set_mouse_cursor("xterm")
    end)

    wp.text_widget:connect_signal("mouse::leave", function(_, find_widgets_result)
        if self:get_focused() == false then
            set_mouse_cursor("left_ptr")
        end

        find_widgets_result.drawable:disconnect_signal("mouse::move", on_drag)
        if wp.unfocus_on_mouse_leave then
            self:unfocus()
        end
    end)

    self:set_widget(widget_template)
end

function text_input:get_mode()
    return self._private.mode
end

function text_input:set_focused(focused)
    if focused == true then
       self:focus()
    else
        self:unfocus()
    end
end

function text_input:set_pattern(pattern)
    self._private.pattern = text_input.patterns[pattern]
end

function text_input:set_obscure(obscure)
    self._private.obscure = obscure
    self:set_text(self:get_text())
end

function text_input:toggle_obscure()
    self:set_obscure(not self._private.obscure)
end

function text_input:set_initial(initial)
    self._private.initial = initial
    self:replace_text(initial)
end

function text_input:update_text(text)
    if self:get_mode() == "insert" then
        self:insert_text(text)
    else
        self:overwrite_text(text)
    end
end

function text_input:set_text(text)
    self._private.text_buffer = text

    if self:get_obscure() then
        self:get_text_widget():set_text(string.rep("*", #text))
    else
        self:get_text_widget():set_text(text)
    end
end

function text_input:replace_text(text)
    self._private.text_buffer = text

    if self:get_obscure() then
        self:set_text(string.rep("*", #text))
    else
        self:set_text(text)
    end

    self:set_cursor_index(#text)
end

function text_input:insert_text(text)
    local wp = self._private

    local old_text = self:get_text()
    local cursor_index = self:get_cursor_index()
    local left_text = old_text:sub(1, cursor_index) .. text
    local right_text = old_text:sub(cursor_index + 1)
    local new_text = left_text .. right_text
    if wp.pattern then
        new_text = new_text:match(wp.pattern)
        if new_text then
            self:set_text(new_text)
            self:set_cursor_index(self:get_cursor_index() + #text)
            self:emit_signal("property::text", self:get_text())
        end
    else
        self:set_text(new_text)
        self:set_cursor_index(self:get_cursor_index() + #text)
        self:emit_signal("property::text", self:get_text())
    end
end

function text_input:overwrite_text(text)
    local wp = self._private

    local start_pos = wp.selection_start
    local end_pos = wp.selection_end
    if start_pos > end_pos then
        start_pos, end_pos = end_pos, start_pos
    end

    local old_text = self:get_text()
    local left_text = old_text:sub(1, start_pos)
    local right_text = old_text:sub(end_pos + 1)
    local new_text = left_text .. text .. right_text

    if wp.pattern then
        new_text = new_text:match(wp.pattern)
        if new_text then
            self:set_text(new_text)
            self:set_cursor_index(#left_text + 1)
            self:emit_signal("property::text", self:get_text())
        end
    else
        self:set_text(new_text)
        self:set_cursor_index(#left_text + 1)
        self:emit_signal("property::text", self:get_text())
    end
end

function text_input:copy()
    local wp = self._private
    if self:get_mode() == "overwrite" then
        local text = self:get_text()
        local start_pos = self._private.selection_start
        local end_pos = self._private.selection_end
        if start_pos > end_pos then
            start_pos, end_pos = end_pos + 1, start_pos
        end
        text = text:sub(start_pos, end_pos)
        wp.clipboard:set_text(text, -1)
    end
end

function text_input:paste()
    local wp = self._private

    wp.clipboard:request_text(function(clipboard, text)
        if text then
            self:update_text(text)
        end
    end)
end

function text_input:delete_next_word()
    local old_text = self:get_text()
    local cursor_index = self:get_cursor_index()

    local left_text = old_text:sub(1, cursor_index)
    local right_text = old_text:sub(cword_end(old_text, cursor_index + 1))
    self:set_text(left_text .. right_text)
    self:emit_signal("property::text", self:get_text())
end

function text_input:delete_previous_word()
    local old_text = self:get_text()
    local cursor_index = self:get_cursor_index()
    local wstart = cword_start(old_text, cursor_index + 1) - 1
    local left_text = old_text:sub(1, wstart)
    local right_text = old_text:sub(cursor_index + 1)
    self:set_text(left_text .. right_text)
    self:set_cursor_index(wstart)
    self:emit_signal("property::text", self:get_text())
end

function text_input:delete_text()
    if self:get_mode() == "insert" then
        self:delete_text_before_cursor()
    else
        self:overwrite_text("")
    end
end

function text_input:delete_text_before_cursor()
    local cursor_index = self:get_cursor_index()
    if cursor_index > 0 then
        local old_text = self:get_text()
        local left_text = old_text:sub(1, cursor_index - 1)
        local right_text = old_text:sub(cursor_index + 1)
        self:set_text(left_text .. right_text)
        self:set_cursor_index(cursor_index - 1)
        self:emit_signal("property::text", self:get_text())
    end
end

function text_input:delete_text_after_cursor()
    local cursor_index = self:get_cursor_index()
    if cursor_index < #self:get_text() then
        local old_text = self:get_text()
        local left_text = old_text:sub(1, cursor_index)
        local right_text = old_text:sub(cursor_index + 2)
        self:set_text(left_text .. right_text)
        self:emit_signal("property::text", self:get_text())
    end
end

function text_input:get_text()
    return self._private.text_buffer or ""
end

function text_input:get_text_widget()
    return self._private.text_widget
end

function text_input:show_selection()
    self._private.selection_opacity = 1
    self:get_text_widget():emit_signal("widget::redraw_needed")
end

function text_input:hide_selection()
    self._private.selection_opacity = 0
    self:get_text_widget():emit_signal("widget::redraw_needed")
end

function text_input:select_all()
    if self:get_text() == "" then
        return
    end

    self:set_selection_start_index(0)
    self:set_selection_end_index(#self:get_text())
end

function text_input:set_selection_to_word()
    if self:get_text() == "" then
        return
    end

    local word_start_index = cword_start(self:get_text(), self:get_cursor_index() + 1) - 1
    local word_end_index = cword_end(self:get_text(), self:get_cursor_index() + 1) - 1

    self:set_selection_start_index(word_start_index)
    self:set_selection_end_index(word_end_index)
end

function text_input:set_selection_start_index(index)
    if #self:get_text() == 0 then
        return
    end

    index = math.max(math.min(index, #self:get_text()), 0)

    local layout = self:get_text_widget()._private.layout
    local strong_pos, weak_pos = layout:get_caret_pos(index)
    if strong_pos then
        self._private.selection_start = index
        self._private.selection_start_x = strong_pos.x / Pango.SCALE
        self._private.selection_start_y = strong_pos.y / Pango.SCALE
        self:get_text_widget():emit_signal("widget::redraw_needed")
    end
end

function text_input:set_selection_end_index(index)
    if #self:get_text() == 0 then
        return
    end

    index = math.max(math.min(index, #self:get_text()), 0)

    local layout = self:get_text_widget()._private.layout
    local strong_pos, weak_pos = layout:get_caret_pos(index)
    if strong_pos then
        if self:get_mode() ~= "overwrite" and index ~= self._private.selection_start  then
            self._private.mode = "overwrite"
            self:show_selection()
            self:hide_cursor()
        end

        self._private.selection_end = index
        self._private.selection_end_x = strong_pos.x / Pango.SCALE
        self._private.selection_end_y = strong_pos.y / Pango.SCALE
        self:get_text_widget():emit_signal("widget::redraw_needed")
    end
end

function text_input:increamant_selection_end_index()
    if self:get_mode() == "insert" then
        self:set_selection_start_index(self:get_cursor_index())
        self:set_selection_end_index(self:get_cursor_index() + 1)
    else
        self:set_selection_end_index(self._private.selection_end + 1)
    end
end

function text_input:decremeant_selection_end_index()
    if self:get_mode() == "insert" then
        self:set_selection_start_index(self:get_cursor_index())
        self:set_selection_end_index(self:get_cursor_index() - 1)
    else
        self:set_selection_end_index(self._private.selection_end - 1)
    end
end

function text_input:set_selection_start_index_from_x_y(x, y)
    local layout = self:get_text_widget()._private.layout
    local index, trailing = layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE)
    if index then
        self:set_selection_start_index(index)
    else
        local pixel_rect, logical_rect = self:get_text_widget()._private.layout:get_pixel_extents()
        if x < logical_rect.x + logical_rect.width then
            self:set_selection_start_index(0)
        else
            self:set_selection_start_index(#self:get_text())
        end
    end
end

function text_input:set_selection_end_index_from_x_y(x, y)
    local layout = self:get_text_widget()._private.layout
    local index, trailing = layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE)
    if index then
        self:set_selection_end_index(index + trailing)
    else
        local pixel_rect, logical_rect = self:get_text_widget()._private.layout:get_pixel_extents()
        if x < logical_rect.x + logical_rect.width then
            self:set_selection_end_index(0)
        else
            self:set_selection_end_index(#self:get_text())
        end
    end
end

function text_input:show_cursor()
    self._private.cursor_opacity = 1
    self:get_text_widget():emit_signal("widget::redraw_needed")
end

function text_input:hide_cursor()
    self._private.cursor_opacity = 0
    self:get_text_widget():emit_signal("widget::redraw_needed")
end

function text_input:set_cursor_index(index)
    index = math.max(math.min(index, #self:get_text()), 0)

    local layout = self:get_text_widget()._private.layout
    local strong_pos, weak_pos = layout:get_cursor_pos(index)
    if strong_pos then
        if strong_pos == self._private.cursor_index and self._private.mode == "insert" then
            return
        end

        if self:get_focused() and self:get_mode() ~= "insert" then
            self:show_cursor()
        end

        self._private.cursor_index = index
        self._private.mode = "insert"

        self._private.cursor_x = strong_pos.x / Pango.SCALE
        self._private.cursor_y = strong_pos.y / Pango.SCALE

        self:hide_selection()

        self:get_text_widget():emit_signal("widget::redraw_needed")
    end
end

function text_input:set_cursor_index_from_x_y(x, y)
    local layout = self:get_text_widget()._private.layout
    local index, trailing = layout:xy_to_index(x * Pango.SCALE, y * Pango.SCALE)

    if index then
        self:set_cursor_index(index)
    else
        local pixel_rect, logical_rect = self:get_text_widget()._private.layout:get_pixel_extents()
        if x < logical_rect.x + logical_rect.width then
            self:set_cursor_index(0)
        else
            self:set_cursor_index(#self:get_text())
        end
    end
end

function text_input:set_cursor_index_to_word_start()
    self:set_cursor_index(cword_start(self:get_text(), self:get_cursor_index() + 1) - 1)
end

function text_input:set_cursor_index_to_word_end()
    self:set_cursor_index(cword_end(self:get_text(), self:get_cursor_index() + 1) - 1)
end

function text_input:set_cursor_index_to_end()
    self:set_cursor_index(#self:get_text())
end

function text_input:increamant_cursor_index()
    if self:get_mode() == "insert" then
        self:set_cursor_index(self:get_cursor_index() + 1)
    else
        local start_pos = self._private.selection_start
        local end_pos = self._private.selection_end
        if start_pos > end_pos then
            start_pos, end_pos = end_pos, start_pos
        end
        self:set_cursor_index(end_pos)
    end
end

function text_input:decremeant_cursor_index()
    if self:get_mode() == "insert" then
        self:set_cursor_index(self:get_cursor_index() - 1)
    else
        local start_pos = self._private.selection_start
        local end_pos = self._private.selection_end
        if start_pos > end_pos then
            start_pos, end_pos = end_pos, start_pos
        end
        self:set_cursor_index(start_pos)
    end
end

function text_input:get_cursor_index()
    return self._private.cursor_index
end

function text_input:set_focus_on_subject_mouse_enter(subject)
    subject:connect_signal("mouse::enter", function()
        self:focus()
    end)
end

function text_input:set_unfocus_on_subject_mouse_leave(subject)
    subject:connect_signal("mouse::leave", function()
        self:unfocus()
    end)
end

function text_input:get_focused()
    return self._private.focused
end

function text_input:focus()
    local wp = self._private

    if self:get_focused() == true then
        return
    end

    -- Do it first, so the cursor won't change back when unfocus was called on the focused text input
    capi.awesome.emit_signal("text_input::focus", self)

    set_mouse_cursor("xterm")

    if self:get_mode() == "insert" then
        self:show_cursor()
    end

    run_keygrabber(self)

    if wp.cursor_blink then
        if wp.cursor_blink_timer == nil then
            wp.cursor_blink_timer = gtimer {
                timeout = wp.cursor_blink_rate,
                autostart = false,
                call_now = false,
                single_shot = false,
                callback = function()
                    if self._private.cursor_opacity == 1 then
                        self:hide_cursor()
                    elseif self:get_mode() == "insert" then
                        self:show_cursor()
                    end
                end
            }
        end
        wp.cursor_blink_timer:start()
    end


    wp.focused = true
    self:emit_signal("focus")
end

function text_input:unfocus(context)
    local wp = self._private
    if self:get_focused() == false then
        return
    end

    set_mouse_cursor("left_ptr")
    self:hide_cursor()
    wp.cursor_blink_timer:stop()
    self:hide_selection()
    if self.reset_on_unfocus == true then
        self:replace_text("")
    end

    awful.keygrabber.stop(wp.keygrabber)
    wp.focused = false
    self:emit_signal("unfocus", context or "normal", self:get_text())
end

function text_input:toggle()
    local wp = self._private

    if self:get_focused() == false then
        self:focus()
    else
        self:unfocus()
    end
end

local function new()
    local widget = wibox.container.background()
    gtable.crush(widget, text_input, true)

    local wp = widget._private

    wp.focused = false
    wp.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
    wp.cursor_index = 0
    wp.mode = "insert"
    wp.click_count = 0

    wp.cursor_x = 0
    wp.cursor_y = 0
    wp.cursor_opacity = 0
    wp.selection_start_x = 0
    wp.selection_end_x = 0
    wp.selection_start_y = 0
    wp.selection_end_y = 0
    wp.selection_opacity = 0

    wp.click_timeout = 0.2

    wp.unfocus_keys = { "Escape", "Return" }
    wp.unfocus_on_root_clicked = true
    wp.unfocus_on_client_clicked = true
    wp.unfocus_on_mouse_leave = false
    wp.unfocus_on_tag_change = true
    wp.unfocus_on_other_text_input_focus = true
    wp.unfocus_on_client_focus = true

    wp.focus_on_subject_mouse_enter = nil
    wp.unfocus_on_subject_mouse_leave = nil

    wp.reset_on_unfocus = false

    wp.pattern = nil
    wp.obscure = false

    wp.placeholder = ""
    wp.text_color = beautiful.colors.on_background
    wp.text = ""

    wp.cursor_width = 2
    wp.cursor_bg = beautiful.colors.on_background
    wp.cursor_blink = true
    wp.cursor_blink_rate = 0.6

    wp.selection_bg = beautiful.colors.random_accent_color()

    widget:set_widget_template(wibox.widget {
        layout = wibox.layout.stack,
        {
            widget = wibox.widget.textbox,
            id = "placeholder_role",
            text = wp.placeholder
        },
        {
            widget = wibox.widget.textbox,
            id = "text_role",
            text = wp.text
        }
    })

    capi.tag.connect_signal("property::selected", function()
        if wp.unfocus_on_tag_change then
            widget:unfocus()
        end
    end)

    capi.awesome.connect_signal("text_input::focus", function(text_input)
        if wp.unfocus_on_other_text_input_focus and text_input ~= widget then
            widget:unfocus()
        end
    end)

    capi.client.connect_signal("focus", function()
        if wp.unfocus_on_client_focus then
            widget:unfocus()
        end
    end)

    awful.mouse.append_global_mousebindings({
        awful.button({"Any"}, 1, function()
            if wp.unfocus_on_root_clicked then
                widget:unfocus()
            end
        end),
        awful.button({"Any"}, 3, function()
            if wp.unfocus_on_root_clicked then
                widget:unfocus()
            end
        end)
    })

    capi.client.connect_signal("button::press", function()
        if wp.unfocus_on_client_clicked then
            widget:unfocus()
        end
    end)

    capi.awesome.connect_signal("colorscheme::changed", function(old_colorscheme_to_new_map)
        wp.text_color = old_colorscheme_to_new_map[wp.text_color]
        wp.cursor_bg = old_colorscheme_to_new_map[wp.cursor_bg]
        wp.selection_bg = old_colorscheme_to_new_map[wp.selection_bg]
    end)

    return widget
end

function text_input.mt:__call(...)
    return new()
end

build_properties(text_input, properties)

return setmetatable(text_input, text_input.mt)
