---------------------------------------------------------------------------- --- Basic Calculator Widget -- -- -- For more details check my repos README.md -- -- -- @author manilarome <gerome.matilla07@gmail.com> -- @copyright 2019 manilarome -- @widget calculator ---------------------------------------------------------------------------- -- A basic calculator widget -- Supports keyboard input! -- Just hover your cursor above the calculator widget and start typing -- Stop keygrabbing by leaving the calculator local awful = require('awful') local wibox = require('wibox') local gears = require('gears') local beautiful = require('beautiful') local dpi = beautiful.xresources.apply_dpi local clickable_container = require('widget.clickable-container') local config_dir = gears.filesystem.get_configuration_dir() local widget_icon_dir = config_dir .. 'widget/calculator/icons/' local calculator_screen = wibox.widget { { id = 'calcu_screen', text = '0', font = 'SF Pro Text Regular 20', align = 'right', valign = 'center', widget = wibox.widget.textbox, }, margins = dpi(5), widget = wibox.container.margin } -- Evaluate local calculate = function () local calcu_screen = calculator_screen.calcu_screen local string_expression = calcu_screen:get_text() if string_expression:sub(-1):match("[%+%-%/%*%^%.]") then return end local func = assert(load("return " .. string_expression)) local ans = tostring(func()) -- Convert -nan to undefined if ans == '-nan' then calcu_screen:set_text('undefined') return end -- Set the answer in textbox calcu_screen:set_text(ans) end local txt_on_screen = function() screen_text = calculator_screen.calcu_screen:get_text() return screen_text == 'inf' or screen_text == 'undefined' or screen_text == 'SYNTAX ERROR' or #screen_text == 1 end -- Delete the last digit in screen local delete_value = function() calcu_screen = calculator_screen.calcu_screen -- Set the screen text to 0 if conditions met if txt_on_screen() then calcu_screen:set_text('0') else -- Delete the last digit calcu_screen:set_text(calcu_screen:get_text():sub(1, -2)) end end -- Clear screen local clear_screen = function() calculator_screen.calcu_screen:set_text('0') end -- The one that filters and checks the user input to avoid errors and bugs local format_screen = function(value) local calcu_screen = calculator_screen.calcu_screen -- If the screen has only 0 if calcu_screen:get_text() == '0' then -- Check if the button pressed sends a value of either +, -, /, *, ^, . if value:sub(-1):match("[%+%/%*%^%.]") then calcu_screen:set_text(calcu_screen:get_text() .. tostring(value)) else calcu_screen:set_text(value) end elseif calcu_screen:get_text() == 'inf' or calcu_screen:get_text() == 'undefined' or calcu_screen:get_text() == 'SYNTAX ERROR' then -- Clear screen if an operator is selected if value:sub(-1):match("[%+%/%*%^%.]") then clear_screen() else -- Replace screen txt with the number value pressed clear_screen() calcu_screen:set_text(tostring(value)) end else -- Don't let the user to input two or more consecutive arithmetic operators and decimals if calcu_screen:get_text():sub(-1):match("[%+%-%/%*%^%.]") and value:sub(-1):match("[%+%-%/%*%^%.%%]") then -- Get the operator from button pressed local string_eval = calcu_screen:get_text():sub(-1):gsub("[%+%-%/%*%^%.]", value) -- This will prevent the user to input consecutive operators and decimals -- It will replace the previous operator with the value of input calcu_screen:set_text(calcu_screen:get_text():sub(1, -2)) -- Concatenate the value operator to the screen string to replace the deleted operator calcu_screen:set_text(calcu_screen:get_text() .. tostring(string_eval)) else -- Concatenate the value to screen string calcu_screen:set_text(calcu_screen:get_text() .. tostring(value)) end end end -- Shape generator local build_shape = function (position, radius) -- Position represents the position of rounded corners if position == 'top' then return function(cr, width, height) gears.shape.partially_rounded_rect(cr, width, height, true, true, false, false, radius) end elseif position == 'top_left' then return function(cr, width, height) gears.shape.partially_rounded_rect(cr, width, height, true, false, false, false, radius) end elseif position == 'top_right' then return function(cr, width, height) gears.shape.partially_rounded_rect(cr, width, height, false, true, false, false, radius) end elseif position == 'bottom_right' then return function(cr, width, height) gears.shape.partially_rounded_rect(cr, width, height, false, false, true, false, radius) end elseif position == 'bottom_left' then return function(cr, width, height) gears.shape.partially_rounded_rect(cr, width, height, false, false, false, true, radius) end else return function(cr, width, height) gears.shape.rounded_rect(cr, width, height, radius) end end end -- Themes widgets local decorate_widget = function(widget_arg, pos, rad) return wibox.widget { widget_arg, bg = beautiful.groups_bg, shape = build_shape(pos, rad), widget = wibox.container.background } end -- Build a button local build_button_widget = function(text, rcp, rad) local value = text local build_textbox = wibox.widget { { id = 'btn_name', text = value, font = 'SF Pro Text 12', align = 'center', valign = 'center', widget = wibox.widget.textbox, }, margins = dpi(5), widget = wibox.container.margin } local build_button = wibox.widget { { build_textbox, margins = dpi(7), widget = wibox.container.margin }, widget = clickable_container } build_button:buttons( gears.table.join( awful.button( {}, 1, nil, function () if value == 'C' then clear_screen() elseif value == '=' then -- Calculate and error handling if not pcall(calculate) then calculator_screen.calcu_screen:set_text('SYNTAX ERROR') end elseif value == 'DEL' then delete_value() else format_screen(value) end end ) ) ) return decorate_widget(build_button, rcp, rad) end local keygrab_running = false local kb_imagebox = wibox.widget { id = 'kb_icon', image = widget_icon_dir .. 'kb-off' .. '.svg', resize = true, forced_height = dpi(15), widget = wibox.widget.imagebox } local kb_button_widget = wibox.widget { { { { layout = wibox.layout.align.horizontal, expand = 'none', nil, kb_imagebox, nil }, margins = dpi(10), widget = wibox.container.margin }, widget = clickable_container }, bg = beautiful.groups_bg, shape = build_shape('bottom_left', beautiful.groups_radius), widget = wibox.container.background } local toggle_btn_keygrab = function() if keygrab_running then kb_imagebox:set_image(widget_icon_dir .. 'kb-off' .. '.svg') awesome.emit_signal("widget::calc_stop_keygrab") keygrab_running = false else kb_imagebox:set_image(widget_icon_dir .. 'kb' .. '.svg') awesome.emit_signal("widget::calc_start_keygrab") keygrab_running = true end end local kb_button = kb_button_widget kb_button:buttons( gears.table.join( awful.button( {}, 1, nil, function() toggle_btn_keygrab() end ) ) ) local calcu_keygrabber = awful.keygrabber { auto_start = true, stop_event = 'release', start_callback = function() keygrab_running = true kb_imagebox:set_image(widget_icon_dir .. 'kb' .. '.svg') end, stop_callback = function() keygrab_running = false kb_imagebox:set_image(widget_icon_dir .. 'kb-off' .. '.svg') end, keypressed_callback = function(self, mod, key, command) if #key == 1 and (key:match('%d+') or key:match('[%+%-%/%*%^%.]')) then format_screen(key) elseif key == 'BackSpace' then delete_value() elseif key == 'Escape' then clear_screen() elseif key == 'x' then awesome.emit_signal("widget::calc_stop_keygrab") elseif key == '=' or key == 'Return' then -- Calculate if not pcall(calculate) then calculator_screen.calcu_screen:set_text('SYNTAX ERROR') end end end, } local calculator_body = wibox.widget { layout = wibox.layout.fixed.vertical, spacing = dpi(1), { spacing = dpi(1), layout = wibox.layout.flex.horizontal, decorate_widget(calculator_screen, 'top', beautiful.groups_radius), }, { spacing = dpi(1), layout = wibox.layout.flex.horizontal, build_button_widget('C', 'flat' , 0), build_button_widget('^', 'flat', 0), build_button_widget('/', 'flat', 0), build_button_widget('DEL', 'flat', 0), }, { spacing = dpi(1), layout = wibox.layout.flex.horizontal, build_button_widget('7', 'flat', 0), build_button_widget('8', 'flat', 0), build_button_widget('9', 'flat', 0), build_button_widget('*', 'flat', 0), }, { spacing = dpi(1), layout = wibox.layout.flex.horizontal, build_button_widget('4', 'flat', 0), build_button_widget('5', 'flat', 0), build_button_widget('6', 'flat', 0), build_button_widget('-', 'flat', 0), }, { spacing = dpi(1), layout = wibox.layout.flex.horizontal, build_button_widget('1', 'flat', 0), build_button_widget('2', 'flat', 0), build_button_widget('3', 'flat', 0), build_button_widget('+', 'flat', 0), }, { spacing = dpi(1), layout = wibox.layout.flex.horizontal, kb_button, build_button_widget('0', 'flat', 0), build_button_widget('.', 'flat', 0), build_button_widget('=', 'bottom_right', beautiful.groups_radius) }, } calculator_body:connect_signal( "mouse::enter", function() -- Start keygrabbing calcu_keygrabber:start() end ) calculator_body:connect_signal( "mouse::leave", function() -- Stop keygrabbing calcu_keygrabber:stop() end ) awesome.connect_signal( "widget::calc_start_keygrab", function() -- Stop keygrabbing calcu_keygrabber:start() end ) awesome.connect_signal( "widget::calc_stop_keygrab", function() -- Stop keygrabbing calcu_keygrabber:stop() end ) local calcu_tooltip = awful.tooltip { objects = {kb_button}, mode = 'outside', align = 'right', delay_show = 1, preferred_positions = {'right', 'left', 'top', 'bottom'}, margin_leftright = dpi(8), margin_topbottom = dpi(8), markup = [[ Tips: Enable keyboard support by hovering your mouse above the calculator. Or toggle it on/off by pressing the keyboard button. Only numbers, arithmetic operators, and decimal point is accepted. Keyboard bindings: = and Return to get the answer. BackSpace to delete the last digit. Escape clears the screen. x stops keygrabbing. Note: While in keygrabbing mode, your keyboard's focus will be on the calculator. So you're AwesomeWM keybinding will stop working. Stopping the keygrabbing mode: * Move away your cursor from the calculator. * Toggle it off using the keyboard button. * Press x. ]] } return calculator_body