diff --git a/calculator/README.md b/calculator/README.md new file mode 100644 index 0000000..448a677 --- /dev/null +++ b/calculator/README.md @@ -0,0 +1,99 @@ +# Calculator + +A calculator for the Noctalia bar. It evaluates whole expressions with operator +precedence, offers both a keypad and a typed input line in its panel, and keeps +the last result visible in the bar. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `yuuto/calculator` | +| Entries | Bar widget: `bar`; panel: `panel` | + +The `panel` entry owns the calculator model and publishes it through Noctalia's +per-plugin state channel. The `bar` widget is a thin client: it reads that state +and opens the panel. + +## Usage + +Add the `bar` bar widget to your bar. It shows a calculator glyph, plus the last +result once there is one. Left click opens the panel, right click clears the +calculator. On a vertical bar only the glyph is shown, with the current +expression as its tooltip. + +The panel evaluates as you type. The small line shows the expression, the large +line the live result, and the copy button next to it puts the result on the +clipboard. + +Enter expressions either with the keypad or by typing into the input line, where +Enter evaluates. Pressing `=` keeps the result in the input, so the next +operator continues from it. The keypad uses plain text labels (`AC`, `+/-`, +`DEL`, `*`, `/`) rather than symbol glyphs, so it renders with any bar font. + +Open the panel over IPC: + +```sh +noctalia msg panel-toggle yuuto/calculator:panel +``` + +### Expression syntax + +Operators are `+`, `-`, `*`, `/` and `^`, with parentheses and the usual +precedence; `^` is right associative. A trailing `%` divides by 100, so +`200*15%` is `30`. The typed input also accepts `×`, `÷` and `−`. + +Available constants: `pi`, `tau`, `e`. + +Available functions: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, +`sinh`, `cosh`, `tanh`, `ln`, `log`, `log2`, `log10`, `exp`, `sqrt`, `cbrt`, +`abs`, `floor`, `ceil`, `round`, `trunc`, `sign`, `mod`, `pow`, `hypot`, `min`, +`max`. + +`log(x)` is base 10; `log(x, b)` is base `b`. `mod(a, b)` is the remainder, since +`%` means percent here. The trigonometric functions follow the angle unit +setting, shown in the panel header as `RAD` or `DEG`. + +## Settings + +### Plugin + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `precision` | `int` | `8` | Maximum decimals used when formatting results, 0 to 10. | +| `angle_unit` | `select` | `rad` | Angle unit for the trigonometric functions. | + +### Bar Widget + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `show_bar_value` | `bool` | `true` | Shows the last result next to the calculator glyph. | +| `max_bar_length` | `int` | `9` | Longer results are shortened with a trailing `~`. | + +## IPC + +Beyond opening the panel, the `panel` entry accepts three events. The `all` +target addresses every live instance. + +```sh +noctalia msg plugin yuuto/calculator:panel all eval "2+3*4" +noctalia msg plugin yuuto/calculator:panel all insert "+8" +noctalia msg plugin yuuto/calculator:panel all clear +``` + +`eval` replaces the expression and evaluates it, `insert` appends to the current +expression, and `clear` resets the calculator. + +## Notes + +Expressions are tokenized and parsed by the plugin itself, so evaluating never +runs a shell command and no external tool is required. Results reach the +clipboard through `noctalia.copyToClipboard`. + +The panel keeps its model in the plugin state channel rather than in script +locals, so clearing from the bar widget and reopening the panel stay in sync. + +## Credits + +Keypad layout and feature set are modelled on the v4 Quickshell calculator plugin +by pir0c0pter0 (MIT). diff --git a/calculator/bar.luau b/calculator/bar.luau new file mode 100644 index 0000000..b36a46c --- /dev/null +++ b/calculator/bar.luau @@ -0,0 +1,94 @@ +--!nonstrict + +local PANEL_ID = "yuuto/calculator:panel" + +local result = "0" +local expression = "" +local hasError = false + +local function maxLength() + local value = noctalia.getConfig("max_bar_length") + if type(value) ~= "number" then + return 9 + end + return math.clamp(math.floor(value), 3, 20) +end + +local function compact(text) + local limit = maxLength() + if #text <= limit then + return text + end + return text:sub(1, math.max(1, limit - 1)) .. "~" +end + +local function badge() + if noctalia.getConfig("show_bar_value") == false then + return "" + end + if hasError then + return noctalia.tr("state.error") + end + if result == "" or result == "0" then + return "" + end + return compact(result) +end + +local function render() + local container = barWidget.isVertical() and ui.column or ui.row + local text = badge() + local color = hasError and "error" or "on_surface" + + barWidget.setTooltip(expression ~= "" and expression or noctalia.tr("bar.tooltip")) + + local children = { + ui.glyph({ name = "calculator", color = color }), + } + + if text ~= "" and not barWidget.isVertical() then + table.insert(children, ui.label({ text = text, fontWeight = "bold", color = color })) + end + + barWidget.render(container({ gap = 6, align = "center" }, children)) +end + +function update() + render() +end + +noctalia.state.watch("calc.result", function(value) + if type(value) == "string" then + result = value + render() + end +end) + +noctalia.state.watch("calc.expression", function(value) + if type(value) == "string" then + expression = value + render() + end +end) + +noctalia.state.watch("calc.error", function(value) + hasError = value == true + render() +end) + +function onClick() + noctalia.togglePanel(PANEL_ID) +end + +function onRightClick() + noctalia.state.set("calc.result", "0") + noctalia.state.set("calc.expression", "") + noctalia.state.set("calc.last_expression", "") + noctalia.state.set("calc.error", false) +end + +function onIpc(event, _payload) + if event == "toggle" then + noctalia.togglePanel(PANEL_ID) + end +end diff --git a/calculator/panel.luau b/calculator/panel.luau new file mode 100644 index 0000000..4ff8c95 --- /dev/null +++ b/calculator/panel.luau @@ -0,0 +1,863 @@ +--!nonstrict + +local DEG = math.pi / 180 + +local WIDE_OPERATORS = { + ["\u{00d7}"] = "*", + ["\u{00f7}"] = "/", + ["\u{2212}"] = "-", + ["\u{2013}"] = "-", + ["\u{00b7}"] = "*", +} + +local CONSTANTS = { + pi = math.pi, + tau = math.pi * 2, + e = math.exp(1), +} + +local ARITY = { + atan2 = 2, + mod = 2, + pow = 2, + hypot = 2, +} + +local function sinh(x) + return (math.exp(x) - math.exp(-x)) / 2 +end + +local function cosh(x) + return (math.exp(x) + math.exp(-x)) / 2 +end + +local function tanh(x) + if x > 20 then + return 1 + end + if x < -20 then + return -1 + end + local a, b = math.exp(x), math.exp(-x) + return (a - b) / (a + b) +end + +local function isDigit(c) + return c >= "0" and c <= "9" +end + +local function isAlpha(c) + return (c >= "a" and c <= "z") or (c >= "A" and c <= "Z") or c == "_" +end + +local function tokenize(src) + local tokens = {} + local i = 1 + local n = #src + + while i <= n do + local wide = WIDE_OPERATORS[src:sub(i, i + 1)] or WIDE_OPERATORS[src:sub(i, i + 2)] + if wide ~= nil then + table.insert(tokens, { kind = "op", text = wide }) + i += WIDE_OPERATORS[src:sub(i, i + 1)] ~= nil and 2 or 3 + continue + end + + local c = src:sub(i, i) + + if c == " " or c == "\t" then + i += 1 + elseif isDigit(c) or (c == "." and isDigit(src:sub(i + 1, i + 1))) then + local start = i + while i <= n and isDigit(src:sub(i, i)) do + i += 1 + end + if src:sub(i, i) == "." then + i += 1 + while i <= n and isDigit(src:sub(i, i)) do + i += 1 + end + end + local mark = i + if src:sub(i, i):lower() == "e" then + local j = i + 1 + local sign = src:sub(j, j) + if sign == "+" or sign == "-" then + j += 1 + end + if isDigit(src:sub(j, j)) then + i = j + while i <= n and isDigit(src:sub(i, i)) do + i += 1 + end + else + i = mark + end + end + local value = tonumber(src:sub(start, i - 1)) + if value == nil then + return nil + end + table.insert(tokens, { kind = "num", value = value }) + elseif isAlpha(c) then + local start = i + while i <= n and (isAlpha(src:sub(i, i)) or isDigit(src:sub(i, i))) do + i += 1 + end + table.insert(tokens, { kind = "name", text = src:sub(start, i - 1):lower() }) + elseif c == "(" or c == ")" then + table.insert(tokens, { kind = c }) + i += 1 + elseif c == "," or c == ";" then + table.insert(tokens, { kind = "," }) + i += 1 + elseif c == "+" or c == "-" or c == "*" or c == "/" or c == "^" or c == "%" then + table.insert(tokens, { kind = "op", text = c }) + i += 1 + else + return nil + end + end + + return tokens +end + +local function makeFunctions(degrees) + local toAngle = degrees and function(x) + return x * DEG + end or function(x) + return x + end + + local fromAngle = degrees and function(x) + return x / DEG + end or function(x) + return x + end + + return { + sin = function(a) + return math.sin(toAngle(a[1])) + end, + cos = function(a) + return math.cos(toAngle(a[1])) + end, + tan = function(a) + return math.tan(toAngle(a[1])) + end, + asin = function(a) + return fromAngle(math.asin(a[1])) + end, + acos = function(a) + return fromAngle(math.acos(a[1])) + end, + atan = function(a) + if #a >= 2 then + return fromAngle(math.atan2(a[1], a[2])) + end + return fromAngle(math.atan(a[1])) + end, + atan2 = function(a) + return fromAngle(math.atan2(a[1], a[2])) + end, + sinh = function(a) + return sinh(a[1]) + end, + cosh = function(a) + return cosh(a[1]) + end, + tanh = function(a) + return tanh(a[1]) + end, + ln = function(a) + return math.log(a[1]) + end, + log = function(a) + if #a >= 2 then + return math.log(a[1]) / math.log(a[2]) + end + return math.log(a[1], 10) + end, + log2 = function(a) + return math.log(a[1], 2) + end, + log10 = function(a) + return math.log(a[1], 10) + end, + exp = function(a) + return math.exp(a[1]) + end, + sqrt = function(a) + return math.sqrt(a[1]) + end, + cbrt = function(a) + local x = a[1] + if x < 0 then + return -((-x) ^ (1 / 3)) + end + return x ^ (1 / 3) + end, + abs = function(a) + return math.abs(a[1]) + end, + floor = function(a) + return math.floor(a[1]) + end, + ceil = function(a) + return math.ceil(a[1]) + end, + round = function(a) + return math.round(a[1]) + end, + trunc = function(a) + local x = a[1] + return x >= 0 and math.floor(x) or math.ceil(x) + end, + sign = function(a) + return math.sign(a[1]) + end, + mod = function(a) + return a[1] % a[2] + end, + pow = function(a) + return a[1] ^ a[2] + end, + hypot = function(a) + return math.sqrt(a[1] * a[1] + a[2] * a[2]) + end, + min = function(a) + return math.min(table.unpack(a)) + end, + max = function(a) + return math.max(table.unpack(a)) + end, + } +end + +local function evaluate(source, degrees) + local tokens = tokenize(source) + if tokens == nil or #tokens == 0 then + return nil + end + + local functions = makeFunctions(degrees) + local pos = 1 + local failed = false + + local function peek() + return tokens[pos] + end + + local function fail() + failed = true + return 0 + end + + local parseExpression + local parseUnary + + local function parsePrimary() + local token = peek() + if token == nil then + return fail() + end + + if token.kind == "num" then + pos += 1 + return token.value + end + + if token.kind == "(" then + pos += 1 + local value = parseExpression() + local closing = peek() + if closing == nil or closing.kind ~= ")" then + return fail() + end + pos += 1 + return value + end + + if token.kind == "name" then + pos += 1 + local name = token.text + local following = peek() + + if following ~= nil and following.kind == "(" then + local fn = functions[name] + if fn == nil then + return fail() + end + pos += 1 + local args = {} + if peek() ~= nil and peek().kind ~= ")" then + table.insert(args, parseExpression()) + while peek() ~= nil and peek().kind == "," do + pos += 1 + table.insert(args, parseExpression()) + end + end + local closing = peek() + if closing == nil or closing.kind ~= ")" then + return fail() + end + pos += 1 + if #args < (ARITY[name] or 1) then + return fail() + end + return fn(args) + end + + local constant = CONSTANTS[name] + if constant ~= nil then + return constant + end + return fail() + end + + return fail() + end + + local function parsePostfix() + local value = parsePrimary() + while true do + local token = peek() + if token ~= nil and token.kind == "op" and token.text == "%" then + pos += 1 + value /= 100 + else + break + end + end + return value + end + + local function parsePower() + local base = parsePostfix() + local token = peek() + if token ~= nil and token.kind == "op" and token.text == "^" then + pos += 1 + return base ^ parseUnary() + end + return base + end + + function parseUnary() + local token = peek() + if token ~= nil and token.kind == "op" and (token.text == "-" or token.text == "+") then + pos += 1 + local value = parseUnary() + return token.text == "-" and -value or value + end + return parsePower() + end + + local function parseTerm() + local value = parseUnary() + while true do + local token = peek() + if token ~= nil and token.kind == "op" and (token.text == "*" or token.text == "/") then + pos += 1 + local rhs = parseUnary() + if token.text == "*" then + value *= rhs + else + if rhs == 0 then + return fail() + end + value /= rhs + end + elseif token ~= nil and token.kind == "(" then + value *= parseUnary() + else + break + end + end + return value + end + + function parseExpression() + local value = parseTerm() + while true do + local token = peek() + if token ~= nil and token.kind == "op" and (token.text == "+" or token.text == "-") then + pos += 1 + local rhs = parseTerm() + if token.text == "+" then + value += rhs + else + value -= rhs + end + else + break + end + end + return value + end + + local ok, value = pcall(parseExpression) + if not ok or failed or pos <= #tokens then + return nil + end + if type(value) ~= "number" or value ~= value or value == math.huge or value == -math.huge then + return nil + end + return value +end + +local function formatNumber(value, precision) + if value == 0 then + return "0" + end + + local magnitude = math.abs(value) + if magnitude >= 1e16 or magnitude < 1e-9 then + local text = string.format("%." .. math.min(precision, 8) .. "e", value) + local mantissa, exponent = text:match("^(.-)e([-+]%d+)$") + if mantissa ~= nil then + if mantissa:find("%.") then + mantissa = mantissa:gsub("0+$", ""):gsub("%.$", "") + end + local sign, digits = exponent:match("^([-+])0*(%d+)$") + return mantissa .. "e" .. (sign == "-" and "-" or "") .. digits + end + return text + end + + local text = string.format("%." .. precision .. "f", value) + if text:find("%.") then + text = text:gsub("0+$", ""):gsub("%.$", "") + end + if text == "-0" then + return "0" + end + return text +end + +local expression = "" +local lastExpression = "" +local result = "0" +local lastPreview = nil +local hasError = false +local inputKey = 0 + +local function precision() + local value = noctalia.getConfig("precision") + if type(value) ~= "number" then + return 8 + end + return math.clamp(math.floor(value), 0, 10) +end + +local function useDegrees() + return noctalia.getConfig("angle_unit") == "deg" +end + +local function restore() + local storedResult = noctalia.state.get("calc.result") + if type(storedResult) == "string" and storedResult ~= "" then + result = storedResult + end + local storedExpression = noctalia.state.get("calc.expression") + if type(storedExpression) == "string" then + expression = storedExpression + end + local storedLast = noctalia.state.get("calc.last_expression") + if type(storedLast) == "string" then + lastExpression = storedLast + end + hasError = noctalia.state.get("calc.error") == true + lastPreview = nil +end + +local function publish() + noctalia.state.set("calc.result", result) + noctalia.state.set("calc.expression", expression) + noctalia.state.set("calc.last_expression", lastExpression) + noctalia.state.set("calc.error", hasError) +end + +local function preview() + if expression == "" then + return nil + end + local value = evaluate(expression, useDegrees()) + if value == nil then + return nil + end + return formatNumber(value, precision()) +end + +local function displayValue() + if hasError then + return noctalia.tr("state.error") + end + if expression == "" then + return result + end + local current = preview() + if current ~= nil then + lastPreview = current + end + return current or lastPreview or result +end + +local function toggleSign() + if expression == "" then + expression = "-" + return + end + + local head, tail = expression:match("^(.-)([%d%.]+)$") + if tail == nil then + local last = expression:sub(-1) + if last:match("[%+%-%*/%^%(]") then + expression ..= "-" + end + return + end + + local before = head:sub(-1) + if before == "-" then + local previous = head:sub(-2, -2) + if head == "-" or previous:match("[%+%-%*/%^%(,]") then + expression = head:sub(1, -2) .. tail + return + end + end + expression = head .. "-" .. tail +end + +local render + +local function clearAll() + expression = "" + lastExpression = "" + result = "0" + lastPreview = nil + hasError = false + inputKey += 1 +end + +local function commit() + if expression == "" then + return + end + + local value = evaluate(expression, useDegrees()) + if value == nil then + hasError = true + lastExpression = expression + expression = "" + lastPreview = nil + inputKey += 1 + return + end + + result = formatNumber(value, precision()) + lastPreview = result + lastExpression = expression + expression = result + hasError = false + inputKey += 1 +end + +local function press(action, literal) + if hasError and action ~= "clear" then + hasError = false + expression = "" + lastExpression = "" + end + + if action == "clear" then + clearAll() + elseif action == "delete" then + expression = expression:sub(1, -2) + inputKey += 1 + elseif action == "sign" then + toggleSign() + inputKey += 1 + elseif action == "equals" then + commit() + else + expression ..= literal + inputKey += 1 + end + + publish() + render() +end + +local KEYPAD = { + { + { label = "AC", handler = "onCalcClear", variant = "destructive" }, + { label = "+/-", handler = "onCalcSign", variant = "secondary" }, + { label = "%", handler = "onCalcPercent", variant = "secondary" }, + { label = "DEL", handler = "onCalcDelete", variant = "secondary" }, + }, + { + { label = "7", handler = "onCalcSeven" }, + { label = "8", handler = "onCalcEight" }, + { label = "9", handler = "onCalcNine" }, + { label = "/", handler = "onCalcDivide", variant = "secondary" }, + }, + { + { label = "4", handler = "onCalcFour" }, + { label = "5", handler = "onCalcFive" }, + { label = "6", handler = "onCalcSix" }, + { label = "*", handler = "onCalcMultiply", variant = "secondary" }, + }, + { + { label = "1", handler = "onCalcOne" }, + { label = "2", handler = "onCalcTwo" }, + { label = "3", handler = "onCalcThree" }, + { label = "-", handler = "onCalcMinus", variant = "secondary" }, + }, + { + { label = "0", handler = "onCalcZero", grow = 2 }, + { label = ".", handler = "onCalcDecimal" }, + { label = "+", handler = "onCalcPlus", variant = "secondary" }, + }, + { + { label = "(", handler = "onCalcOpen", variant = "ghost" }, + { label = ")", handler = "onCalcClose", variant = "ghost" }, + { label = "=", handler = "onCalcEquals", variant = "primary", grow = 2 }, + }, +} + +local function keypadRow(row, rowIndex) + local buttons = {} + for index, spec in ipairs(row) do + table.insert( + buttons, + ui.button({ + key = "k" .. rowIndex .. "-" .. index, + text = spec.label, + variant = spec.variant or "default", + fontSize = 15, + height = 34, + flexGrow = spec.grow or 1, + onClick = spec.handler, + }) + ) + end + return ui.row({ key = "row" .. rowIndex, gap = 6 }, buttons) +end + +function render() + local shown = displayValue() + local hint = hasError and lastExpression or (expression ~= "" and expression or lastExpression) + local atRest = shown == "0" and expression == "" and lastExpression == "" + local canCopy = not hasError and shown ~= "" and not atRest + + local keypad = {} + for index, row in ipairs(KEYPAD) do + table.insert(keypad, keypadRow(row, index)) + end + + panel.render(ui.column({ gap = 10, padding = 14 }, { + ui.row({ gap = 8, align = "center" }, { + ui.glyph({ name = "calculator", size = 16, color = "primary" }), + ui.label({ + text = noctalia.tr("panel.title"), + fontSize = 15, + fontWeight = "bold", + color = "on_surface", + flexGrow = 1, + }), + ui.label({ + text = noctalia.tr(useDegrees() and "panel.deg" or "panel.rad"), + fontSize = 11, + color = "on_surface/0.55", + }), + }), + + ui.column({ gap = 2, padding = 10, fill = "primary/0.08", radius = 10 }, { + ui.label({ + text = hint, + fontSize = 11, + color = "on_surface/0.6", + textAlign = "right", + maxLines = 1, + }), + ui.row({ gap = 6, align = "center" }, { + ui.label({ + text = shown, + fontSize = 30, + fontWeight = "bold", + color = hasError and "error" or "on_surface", + textAlign = "right", + maxLines = 1, + flexGrow = 1, + }), + ui.button({ + key = "copy", + glyph = "copy", + glyphSize = 15, + variant = "ghost", + controlSize = "sm", + contentAlign = "center", + tooltip = noctalia.tr("panel.copy"), + visible = canCopy, + onClick = "onCalcCopy", + }), + }), + }), + + ui.input({ + key = "expr-" .. inputKey, + value = expression, + placeholder = noctalia.tr("panel.placeholder"), + fontSize = 13, + controlSize = "sm", + focus = true, + onChange = "onCalcInputChange", + onSubmit = "onCalcInputSubmit", + }), + + ui.column({ gap = 6 }, keypad), + })) +end + +function onCalcClear() + press("clear") +end + +function onCalcSign() + press("sign") +end + +function onCalcPercent() + press("append", "%") +end + +function onCalcDelete() + press("delete") +end + +function onCalcZero() + press("append", "0") +end + +function onCalcOne() + press("append", "1") +end + +function onCalcTwo() + press("append", "2") +end + +function onCalcThree() + press("append", "3") +end + +function onCalcFour() + press("append", "4") +end + +function onCalcFive() + press("append", "5") +end + +function onCalcSix() + press("append", "6") +end + +function onCalcSeven() + press("append", "7") +end + +function onCalcEight() + press("append", "8") +end + +function onCalcNine() + press("append", "9") +end + +function onCalcDecimal() + press("append", ".") +end + +function onCalcPlus() + press("append", "+") +end + +function onCalcMinus() + press("append", "-") +end + +function onCalcMultiply() + press("append", "*") +end + +function onCalcDivide() + press("append", "/") +end + +function onCalcOpen() + press("append", "(") +end + +function onCalcClose() + press("append", ")") +end + +function onCalcEquals() + press("equals") +end + +function onCalcCopy() + local shown = displayValue() + if hasError or shown == "" then + return + end + if noctalia.copyToClipboard(shown, "text/plain") then + noctalia.notify(noctalia.tr("panel.title"), noctalia.tr("panel.copied", { value = shown })) + end +end + +function onCalcInputChange(value) + expression = value + hasError = false + publish() + render() +end + +function onCalcInputSubmit() + press("equals") +end + +function onOpen(_context) + restore() + inputKey += 1 + render() +end + +function onIpc(event, payload) + if event == "clear" then + clearAll() + elseif event == "eval" and type(payload) == "string" then + expression = payload + commit() + elseif event == "insert" and type(payload) == "string" then + expression ..= payload + inputKey += 1 + else + return + end + publish() + render() +end + +noctalia.state.watch("calc.expression", function(value) + if type(value) ~= "string" or value == expression then + return + end + restore() + inputKey += 1 + render() +end) + +restore() +publish() diff --git a/calculator/plugin.toml b/calculator/plugin.toml new file mode 100644 index 0000000..33ceef9 --- /dev/null +++ b/calculator/plugin.toml @@ -0,0 +1,59 @@ +id = "yuuto/calculator" +name = "Calculator" +version = "1.0.0" +plugin_api = 3 +author = "yuuto" +license = "MIT" +icon = "calculator" +description = "A theme-aware calculator with a bar widget and a panel: full expression evaluation, a button grid, and typed input." +dependencies = [] +tags = ["utility", "productivity", "bar", "panel"] + +[[setting]] +key = "precision" +type = "int" +label_key = "settings.precision.label" +description_key = "settings.precision.description" +default = 8 +min = 0 +max = 10 + +[[setting]] +key = "angle_unit" +type = "select" +label_key = "settings.angle_unit.label" +description_key = "settings.angle_unit.description" +default = "rad" +options = [ + { value = "rad", label_key = "settings.angle_unit.options.rad" }, + { value = "deg", label_key = "settings.angle_unit.options.deg" }, +] + +[[widget]] +id = "bar" +entry = "bar.luau" + + [[widget.setting]] + key = "show_bar_value" + type = "bool" + label_key = "settings.show_bar_value.label" + description_key = "settings.show_bar_value.description" + default = true + + [[widget.setting]] + key = "max_bar_length" + type = "int" + label_key = "settings.max_bar_length.label" + description_key = "settings.max_bar_length.description" + default = 9 + min = 3 + max = 20 + +[[panel]] +id = "panel" +entry = "panel.luau" +width = 320 +height = 452 +placement = "attached" +position = "auto" +open_near_click = true diff --git a/calculator/thumbnail.webp b/calculator/thumbnail.webp new file mode 100644 index 0000000..a373d73 Binary files /dev/null and b/calculator/thumbnail.webp differ diff --git a/calculator/translations/de.json b/calculator/translations/de.json new file mode 100644 index 0000000..d2a556c --- /dev/null +++ b/calculator/translations/de.json @@ -0,0 +1,38 @@ +{ + "bar": { + "tooltip": "Rechner" + }, + "panel": { + "title": "Rechner", + "placeholder": "Ausdruck eingeben, Enter drücken", + "copy": "Ergebnis kopieren", + "copied": "Kopiert: {value}", + "rad": "RAD", + "deg": "GRAD" + }, + "state": { + "error": "Fehler" + }, + "settings": { + "precision": { + "label": "Dezimalgenauigkeit", + "description": "Maximale Anzahl Nachkommastellen bei der Ergebnisformatierung." + }, + "angle_unit": { + "label": "Winkeleinheit", + "description": "Einheit, die die trigonometrischen Funktionen verwenden.", + "options": { + "rad": "Bogenmaß", + "deg": "Grad" + } + }, + "show_bar_value": { + "label": "Wert in der Leiste anzeigen", + "description": "Zeigt das letzte Ergebnis neben dem Rechner-Symbol an." + }, + "max_bar_length": { + "label": "Maximale Länge in der Leiste", + "description": "Längere Ergebnisse werden in der Leiste mit Auslassungspunkten gekürzt." + } + } +} diff --git a/calculator/translations/en.json b/calculator/translations/en.json new file mode 100644 index 0000000..d844fca --- /dev/null +++ b/calculator/translations/en.json @@ -0,0 +1,38 @@ +{ + "bar": { + "tooltip": "Calculator" + }, + "panel": { + "title": "Calculator", + "placeholder": "Type an expression, press Enter", + "copy": "Copy result", + "copied": "Copied {value}", + "rad": "RAD", + "deg": "DEG" + }, + "state": { + "error": "Error" + }, + "settings": { + "precision": { + "label": "Decimal precision", + "description": "Maximum number of decimals used when formatting results." + }, + "angle_unit": { + "label": "Angle unit", + "description": "Unit used by the trigonometric functions.", + "options": { + "rad": "Radians", + "deg": "Degrees" + } + }, + "show_bar_value": { + "label": "Show value in bar", + "description": "Display the last result next to the calculator icon." + }, + "max_bar_length": { + "label": "Maximum bar length", + "description": "Longer results are shortened with an ellipsis in the bar." + } + } +}