Full Code of Stardust-kyun/calla for AI

main 6d7006882f30 cached
61 files
255.7 KB
77.1k tokens
1 requests
Download .txt
Showing preview only (275K chars total). Download the full file or copy to clipboard to get everything.
Repository: Stardust-kyun/calla
Branch: main
Commit: 6d7006882f30
Files: 61
Total size: 255.7 KB

Directory structure:
gitextract_yiu3gkuy/

├── .gitignore
├── DEBIAN/
│   └── control
├── PKGBUILD
├── README.md
├── build.sh
└── src/
    └── usr/
        ├── bin/
        │   └── calla
        └── share/
            ├── calla/
            │   ├── Xresources
            │   ├── compositor.conf
            │   ├── desktop/
            │   │   ├── color/
            │   │   │   ├── .backup/
            │   │   │   │   ├── bloom/
            │   │   │   │   │   └── bloom.json
            │   │   │   │   ├── dark/
            │   │   │   │   │   ├── dark.json
            │   │   │   │   │   └── dark.json.bak
            │   │   │   │   ├── light/
            │   │   │   │   │   └── light.json
            │   │   │   │   ├── sakura/
            │   │   │   │   │   └── sakura.json
            │   │   │   │   ├── shore/
            │   │   │   │   │   └── shore.json
            │   │   │   │   └── wave/
            │   │   │   │       └── wave.json
            │   │   │   ├── dark/
            │   │   │   │   └── dark.json
            │   │   │   ├── desktop.lua
            │   │   │   ├── light/
            │   │   │   │   └── light.json
            │   │   │   ├── solarized/
            │   │   │   │   └── solarized.json
            │   │   │   └── terminal.sh
            │   │   ├── config/
            │   │   │   ├── bind.lua
            │   │   │   ├── init.lua
            │   │   │   ├── main.lua
            │   │   │   ├── rule.lua
            │   │   │   └── shot.lua
            │   │   ├── json.lua
            │   │   ├── rc.lua
            │   │   ├── signal/
            │   │   │   ├── brightness.lua
            │   │   │   ├── desktop.lua
            │   │   │   ├── init.lua
            │   │   │   ├── playerctl.lua
            │   │   │   └── volume.lua
            │   │   └── theme/
            │   │       ├── brightness.lua
            │   │       ├── control/
            │   │       │   ├── calendar.lua
            │   │       │   ├── init.lua
            │   │       │   ├── media.lua
            │   │       │   ├── notifs.lua
            │   │       │   ├── profile.lua
            │   │       │   ├── sliders.lua
            │   │       │   ├── system.lua
            │   │       │   └── toggles.lua
            │   │       ├── desktop.lua
            │   │       ├── dock.lua
            │   │       ├── init.lua
            │   │       ├── launcher.lua
            │   │       ├── lock.lua
            │   │       ├── notif.lua
            │   │       ├── panel.lua
            │   │       ├── preview.lua
            │   │       ├── settings.lua
            │   │       ├── theme.lua
            │   │       ├── title.lua
            │   │       └── volume.lua
            │   └── xsettingsd
            ├── themes/
            │   ├── dark/
            │   │   ├── gtk-3.0/
            │   │   │   └── gtk.css
            │   │   └── index.theme
            │   ├── light/
            │   │   ├── gtk-3.0/
            │   │   │   └── gtk.css
            │   │   └── index.theme
            │   └── solarized/
            │       ├── gtk-3.0/
            │       │   └── gtk.css
            │       └── index.theme
            └── xsessions/
                └── calla.desktop

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
pkg/
*.tar.*
*.deb
package/

================================================
FILE: DEBIAN/control
================================================
Package: calla
Version: 0.3.0-5
Maintainer: Stella <stardust-kyun@proton.me>
Depends: awesome (>=4.3.0.0~git), xorg, pipewire, pipewire-pulse, pulseaudio-utils, wireplumber, brightnessctl, inotify-tools, picom, maim, papirus-icon-theme, xsettingsd, network-manager-gnome, policykit-1-gnome, playerctl, liblua-pam, gir1.2-upowerglib-1.0, xclip, breeze-cursor-theme
Suggests: vim-gtk3, nemo, lollypop, firefox-esr
Architecture: amd64
Homepage: https://github.com/stardust-kyun/calla
Description: Calla Desktop Environment


================================================
FILE: PKGBUILD
================================================
# From @levraiardox 
# To https://github.com/Stardust-kyun/calla

pkgname="calla"
pkgver="0.3"
pkgrel="1"
pkgdesc="Calla desktop environement"
arch=("x86_64")
url="https://github.com/Stardust-kyun/calla"

depends=(
        "xorg-server" 
        "pipewire-pulse"
        "brightnessctl"
        "inotify-tools"
        "awesome-git"
        "picom"
        "maim"
        "papirus-icon-theme"
        "noto-fonts"
        "noto-fonts-cjk"
        "noto-color-emoji-fontconfig"
        "noto-fonts-extra"
        "lua-pam-git"
        )

optdepends=(
        "st: terminal",
        "vim-gtk3: vim with clipboard",
        "nemo: file manager",
        "network-manager-gnome: network applet",
        "polkit-gnome: polkit",
        "cbatticon: battery applet",
        "blueman: bluetooth applet",
        "xdg-user-dirs: generate home directories",
        "lollypop: music player",
        )

package() {
    cp -r "${srcdir}/usr" "${pkgdir}/"
}


================================================
FILE: README.md
================================================
<h1 align=center>Calla</h1>

<div align="center">
<a href="#install">Install</a> - <a href="#usage">Usage</a> - <a href="#gallery">Gallery</a> - <a href="#credit">Credit</a> - <a href="#contact">Contact</a>
</div>

---

![latest](.github/latest.png)

<!-- [showcase](https://github.com/Stardust-kyun/dotfiles/assets/56178655/d52b1338-b3f6-444e-b97c-59bdc2544414) -->

---

## Install

### Read Before Installing

In my previous work, I have provided scripts to make it much easier for the average user to install. However, I have found that these scripts are a massive pain to maintain for a variety of distributions, so this time I have provided universal instructions instead. These instructions have been written based on my Debian installation, so you may find that you have additional steps. If you have any questions, see <a href="#contact">contact</a>.

<details>
<summary><b>Installation</b></summary>

---

# Under Construction

Installation instructions are currently being revised as Calla is packaged for major distros. As of version 0.3.0-1, only Debian is supported. If you are attempting to install Calla on Debian, find the newest release in the releases tab. If you are attempting to install on other distros, make sure you have the following installed:

- AwesomeWM git
- Your distribution's package equivalents for line 4 of `DEBIAN/control` (the line that starts with `Depends:`)

---

</details>

## Usage

<details>
<summary><b>Settings App</b></summary>

---

Calla contains a settings app to configure itself without editing any files. It can be opened through the settings icon in the control center, or by pressing `Mod+Shift+C`.

### General

- Terminal - The terminal to run on `Mod+Enter`
- Shutdown/Reboot - The commands to use to shutdown and reboot the system
- Fallback Password - The password Calla will use if it cannot use your user's password
- Font(s) - Fonts used throughout the desktop environment
- Battery - The name of your system's battery (found in `/sys/class/power_supply/`
- Wallpaper - If you would like to overwrite the theme's default wallpaper
- Screenshot Directory - The directory that screenshots are saved to

### Theme

- Color Scheme - The theme Calla will use
- Colors - The colors the theme will use, in base8
- Compositor Settings - Settings for the shadow picom sets
- Gui/Icon Theme - The names of the gui and icon themes the theme should use

---

</details>

## Gallery

<details>
<summary><b>Screenshots</b></summary>

---

### Apps
![apps](.github/apps.png)

### Launcher
![launcher](.github/launcher.png)

### Tag Preview
![preview](.github/preview.png)

### Volume/Brightness Popup
![volume](.github/volume.png)

### Lock Screen
![lockscreen](.github/lockscreen.png)

---

</details>

## Credit

### Thanks

- [Ardox](https://github.com/LeVraiArdox) for refactoring and adding proper packaging for Arch.
- [Sammy](https://github.com/TorchedSammy) for help understanding and adding live reloading.
- [Crylia](https://github.com/Crylia) for massive amounts of help learning awesomewm.
- [Jimmy](https://github.com/Jimmysit0) and [Petrolblue](https://github.com/petrolblue) for help with color schemes and lots of support.
- And the support of many more!

### Historical Contributions

- [AloneERO](https://gitlab.com/AloneER0) for help adding support for Void, Fedora, OpenSUSE, and Alpine!
- [Frankfut](https://github.com/frankfutlg) for help adding support for Void and lots of help with debugging.
- [Qwickdom](https://github.com/Qwickdom) for help adding support for Arch.
- [Reverse](https://github.com/Reversedc) for help adding support for Debian.
- [Alyssa](https://github.com/alyssa-sudo) for help adding support for Gentoo.

### References

- [Sammy's Dotfiles](https://github.com/TorchedSammy/dotfiles)
- [Saimoom's Dotfiles](https://github.com/saimoomedits/dotfiles)
- [Smeueg's Dotfiles](https://github.com/Smeueg/Dotfiles)
- [Bling](https://github.com/BlingCorp/bling)

### Projects

- [Phocus](https://github.com/phocus/gtk)

## Contact

You can find my contact information on my [website](https://star.is-a.dev/). I also have a [discord server](https://discord.gg/38hQb6V8AW) with help available and updates for when new features are added.

## Stars

![Star History Chart](https://api.star-history.com/svg?repos=Stardust-kyun/calla&type=Date)


================================================
FILE: build.sh
================================================
#!/usr/bin/env bash
url=`cat ./.git/config | grep "url = " | sed "s/^[^=]*= //"`
if [[ $url == "https://github.com/stardust-kyun/calla" ]]; then # make this case insensitive
	read -p "
Which distro would you like to build for?

(1) Debian
(2) Arch

(?) Select option: " dist
	case $dist in
		"1")
			mkdir -p package
			read -p "Version (0.1.0-1): " ver
			cp -r src/usr package/
			cp -r DEBIAN package/
			
			dpkg-deb --build package calla_$ver\_amd64.deb
			rm -rf package
			;;
		"2")
			makepkg -f --noconfirm
			;;
		*)
			echo "Something went wrong. Did you choose an option correctly?"
			exit
	esac
else
	echo "Something went wrong. Are you inside the project directory?"
fi


================================================
FILE: src/usr/bin/calla
================================================
#!/bin/sh
xrdb /usr/share/calla/Xresources
awesome --config /usr/share/calla/desktop/rc.lua


================================================
FILE: src/usr/share/calla/Xresources
================================================
#define BG #f5f5f5
#define FG #303030
#define BL #505050
#define WH #d0d0d0
#define R #ac4142
#define G #90a959
#define Y #f4bf75
#define B #6a9fb5
#define M #aa759f
#define C #75b5aa

st.font: RobotoMono:size=11
st.borderpx: 25

! special
*.foreground:   FG
*.background:   BG
*.cursorColor:  FG

! black
*.color0:       BL
*.color8:       BL

! red
*.color1:       R
*.color9:       R

! green
*.color2:       G
*.color10:      G

! yellow
*.color3:       Y
*.color11:      Y

! blue
*.color4:       B
*.color12:      B

! magenta
*.color5:       M
*.color13:      M

! cyan
*.color6:       C
*.color14:      C

! white
*.color7:       WH
*.color15:      WH


================================================
FILE: src/usr/share/calla/compositor.conf
================================================
#################################
#
# Shadows
#
#################################

# Enabled client-side shadows on windows.
shadow = true;
# The blur radius for shadows. (default 30)
shadow-radius = 30;
# The left offset for shadows. (default -30)
shadow-offset-x = -30;
# The top offset for shadows. (default -30)
shadow-offset-y = -30;
# The translucency for shadows. (default .25)
shadow-opacity = .25;
# The radius of corner rounding. (default 15)
corner-radius = 15;
# Window types to exclude from rounding.
rounded-corners-exclude = [
	"window_type = 'splash'"
];
# Special rules.
corner-radius-rules = [
	"20:window_type = 'desktop'"
];

#################################
#
# Opacity
#
#################################

# Don't make anything transparent!
inactive-opacity = 1;
active-opacity = 1;
frame-opacity = 1;
inactive-opacity-override = false;

#################################
#
# Fading
#
#################################

# Fade windows during opacity changes.
fading = true;
# The time between steps in a fade in milliseconds. (default 3).
fade-delta = 3;
# Opacity change between steps while fading in. (default 0.03).
fade-in-step = 0.03;
# Opacity change between steps while fading out. (default 0.03).
fade-out-step = 0.03;


================================================
FILE: src/usr/share/calla/desktop/color/.backup/bloom/bloom.json
================================================
{
	"bg": "#fffaf5",
	"bgalt": "#ebe6e1",
	"bgmid": "#ebe6e1",
	"black": "#4b4646",
	"blue": "#9bb9f0",
	"compoffset": "-25",
	"compopacity": ".1",
	"compradius": "25",
	"cyan": "#a0e1d2",
	"fg": "#4b4646",
	"green": "#96e6a5",
	"gtk": "bloom",
	"icons": "bloom",
	"magenta": "#d7a0e6",
	"red": "#eb8c8c",
	"wall": "color/bloom/bloom.png",
	"white": "#ebe6e1",
	"yellow": "#f0cd96"
}


================================================
FILE: src/usr/share/calla/desktop/color/.backup/dark/dark.json
================================================
{
	"bg": "#151515",
	"bgalt": "#1a1a1a",
	"bgmid": "#202020",
	"black": "#505050",
	"blue": "#6a9fb5",
	"compoffset": "-25",
	"compopacity": ".5",
	"compradius": "25",
	"cyan": "#75b5aa",
	"fg": "#f5f5f5",
	"green": "#90a959",
	"gtk": "dark",
	"icons": "Papirus-Dark",
	"magenta": "#aa759f",
	"red": "#ac4142",
	"wall": "color/dark/dark.png",
	"white": "#d0d0d0",
	"yellow": "#f4bf75"
}

================================================
FILE: src/usr/share/calla/desktop/color/.backup/dark/dark.json.bak
================================================
{
	"bg": "#151515",
	"bgalt": "#252525",
	"bgmid": "#1d1d1d",
	"black": "#505050",
	"blue": "#6a9fb5",
	"compoffset": "-25",
	"compopacity": ".5",
	"compradius": "25",
	"cyan": "#75b5aa",
	"fg": "#f5f5f5",
	"green": "#90a959",
	"gtk": "dark",
	"icons": "Papirus",
	"magenta": "#aa759f",
	"red": "#ac4142",
	"wall": "color/dark/dark.png",
	"white": "#d0d0d0",
	"yellow": "#f4bf75"
}


================================================
FILE: src/usr/share/calla/desktop/color/.backup/light/light.json
================================================
{
    "bg": "#f5f5f5",
    "bgalt": "#d0d0d0",
    "bgmid": "#e3e3e3",
    "black": "#505050",
    "blue": "#6a9fb5",
    "compoffset": "-25",
    "compopacity": ".5",
    "compradius": "25",
    "cyan": "#75b5aa",
    "fg": "#303030",
    "green": "#90a959",
    "gtk": "light",
    "icons": "Papirus-Light",
    "magenta": "#aa759f",
    "red": "#ac4142",
    "wall": "color/light/light.png",
    "white": "#d0d0d0",
    "yellow": "#f4bf75"
}


================================================
FILE: src/usr/share/calla/desktop/color/.backup/sakura/sakura.json
================================================
{
	"bg": "#000f14",
	"bgalt": "#0a191e",
	"bgmid": "#0a191e",
	"black": "#0a191e",
	"blue": "#326482",
	"compoffset": "-25",
	"compopacity": ".5",
	"compradius": "25",
	"cyan": "#327d7d",
	"fg": "#a0a0b4",
	"green": "#468264",
	"gtk": "sakura",
	"icons": "sakura",
	"magenta": "#645078",
	"red": "#824655",
	"wall": "color/sakura/sakura.png",
	"white": "#a0a0b4",
	"yellow": "#827d50"
}


================================================
FILE: src/usr/share/calla/desktop/color/.backup/shore/shore.json
================================================
{
	"bg": "#19191e",
	"bgalt": "#2b2b33",
	"bgmid": "#2b2b33",
	"black": "#2b2b33",
	"blue": "#505a82",
	"compoffset": "-25",
	"compopacity": ".5",
	"compradius": "25",
	"cyan": "#5a7387",
	"fg": "#9999a8",
	"green": "#5a825a",
	"gtk": "shore",
	"icons": "shore",
	"magenta": "#735a87",
	"red": "#825a5a",
	"wall": "color/shore/shore.png",
	"white": "#9999a8",
	"yellow": "#968264"
}


================================================
FILE: src/usr/share/calla/desktop/color/.backup/wave/wave.json
================================================
{
	"bg": "#f0fafa",
	"bgalt": "#dce6e6",
	"bgmid": "#dce6e6",
	"black": "#404040",
	"blue": "#83b4e6",
	"compoffset": "-25",
	"compopacity": ".1",
	"compradius": "25",
	"cyan": "#8cd7d2",
	"fg": "#262626",
	"green": "#a0e6af",
	"gtk": "wave",
	"icons": "wave",
	"magenta": "#e1aae1",
	"red": "#e68383",
	"wall": "color/wave/wave.png",
	"white": "#dce6e6",
	"yellow": "#ffcd96"
}


================================================
FILE: src/usr/share/calla/desktop/color/dark/dark.json
================================================
{
	"bg": "#151515",
	"bgalt": "#2B2B2B",
	"bgmid": "#202020",
	"black": "#505050",
	"blue": "#6a9fb5",
	"compoffset": "-25",
	"compopacity": ".5",
	"compradius": "25",
	"cyan": "#75b5aa",
	"fg": "#f5f5f5",
	"green": "#90a959",
	"gtk": "dark",
	"icons": "Papirus-Dark",
	"magenta": "#aa759f",
	"red": "#ac4142",
	"wall": "color/dark/dark.png",
	"white": "#d0d0d0",
	"yellow": "#f4bf75"
}

================================================
FILE: src/usr/share/calla/desktop/color/desktop.lua
================================================
local calla="/usr/share/calla/"
local picom=calla.."compositor.conf"
local xresources=calla.."Xresources"
local xsettingsd=calla.."xsettingsd"

local function compositor(radius, x, y, opacity)
	local r = assert(io.open(picom, "r"))
	local file = r:read("*all")
	r:close()
	
	file = file:gsub("shadow%-radius = .-\n", "shadow-radius = "..radius..";\n")
	file = file:gsub("shadow%-offset%-x = .-\n", "shadow-offset-x = "..x..";\n")
	file = file:gsub("shadow%-offset%-y = .-\n", "shadow-offset-y = "..y..";\n")
	file = file:gsub("shadow%-opacity = .-\n", "shadow-opacity = "..opacity..";\n")

	local w = assert(io.open(picom, "w"))
	w:write(file)
	w:close()
end

local function terminal(bg, fg, bl, wh, r, g, y, b, m, c)
	local read = assert(io.open(xresources, "r"))
	local file = read:read("*all")
	read:close()

	file = file:gsub("%#define BG .-\n", "#define BG "..bg.."\n")
	file = file:gsub("%#define FG .-\n", "#define FG "..fg.."\n")
	file = file:gsub("%#define BL .-\n", "#define BL "..bl.."\n")
	file = file:gsub("%#define WH .-\n", "#define WH "..wh.."\n")
	file = file:gsub("%#define R .-\n", "#define R "..r.."\n")
	file = file:gsub("%#define G .-\n", "#define G "..g.."\n")
	file = file:gsub("%#define Y .-\n", "#define Y "..y.."\n")
	file = file:gsub("%#define B .-\n", "#define B "..b.."\n")
	file = file:gsub("%#define M .-\n", "#define M "..m.."\n")
	file = file:gsub("%#define C .-\n", "#define C "..c.."\n")

	local w = assert(io.open(xresources, "w"))
	w:write(file)
	w:close()

	os.execute("xrdb " .. xresources)
	require("awful").spawn.easy_async_with_shell(require("gears").filesystem.get_configuration_dir() .. "color/terminal.sh")
end

local function theme(theme, icon)
	local r = assert(io.open(xsettingsd, "r"))
	local file = r:read("*all")
	r:close()

	file = file:gsub("Net/ThemeName .-\n", "Net/ThemeName \""..theme.."\"\n")
	file = file:gsub("Net/IconThemeName .-\n", "Net/IconThemeName \""..icon.."\"\n")

	local w = assert(io.open(xsettingsd, "w"))
	w:write(file)
	w:close()

	os.execute("xsettingsd --config '/usr/share/calla/xsettingsd' &")
end

awesome.connect_signal("color::change", function(color)
	compositor(color.compradius, color.compoffset, color.compoffset, color.compopacity)

	terminal(color.bg, color.fg, color.black, color.white, color.red, color.green, color.yellow, color.blue, color.magenta, color.cyan)

	theme(color.gtk, color.icons)
end)


================================================
FILE: src/usr/share/calla/desktop/color/light/light.json
================================================
{
	"bg": "#f5f5f5",
	"bgalt": "#d0d0d0",
	"bgmid": "#e3e3e3",
	"black": "#505050",
	"blue": "#6a9fb5",
	"compoffset": "-30",
	"compopacity": ".25",
	"compradius": "30",
	"cyan": "#75b5aa",
	"fg": "#303030",
	"green": "#90a959",
	"gtk": "light",
	"icons": "Papirus-Light",
	"magenta": "#aa759f",
	"red": "#ac4142",
	"wall": "color/light/light.png",
	"white": "#d0d0d0",
	"yellow": "#f4bf75"
}

================================================
FILE: src/usr/share/calla/desktop/color/solarized/solarized.json
================================================
{
	"bg": "#002B36",
	"bgalt": "#063C4A",
	"bgmid": "#043643",
	"black": "#657B83",
	"blue": "#268BD2",
	"compoffset": "-30",
	"compopacity": ".25",
	"compradius": "30",
	"cyan": "#2AA198",
	"fg": "#93A1A1",
	"green": "#859900",
	"gtk": "solarized",
	"icons": "Papirus-Dark",
	"magenta": "#6C71C4",
	"red": "#DC322F",
	"wall": "color/solarized/solarized.png",
	"white": "#FDF6E3",
	"yellow": "#B58900"
}

================================================
FILE: src/usr/share/calla/desktop/color/terminal.sh
================================================
#!/usr/bin/env bash

xrdb_query() {
	value=$(xrdb -query | grep -i "^*\.$1:" | cut -f 2)
	if [ -n "${value}" ]; then
		echo "${value}"
		return 0
	fi
	return 1
}

sequences=""
for i in $(seq 0 15); do
	sequences+="\033]4;${i};$(xrdb_query "color${i}")\007"
done
sequences+="\\033]10;$(xrdb_query foreground)\\007"
sequences+="\\033]11;$(xrdb_query background)\\007"
sequences+="\\033]12;$(xrdb_query cursorColor)\\007"
sequences+="\\033]708;$(xrdb_query background)\\007"

for term in /dev/pts/[0-9]*; do
	printf "%b" "${sequences}" > "${term}" &
done


================================================
FILE: src/usr/share/calla/desktop/config/bind.lua
================================================
local awful = require("awful")
local mod = user.mod

-- Mouse 

client.connect_signal("request::default_mousebindings", function()
    awful.mouse.append_client_mousebindings({

		-- Move

        awful.button({ mod }, 1, function(c)
            c:activate { context = "mouse_click", action = "mouse_move"  }
        end),

		-- Resize

        awful.button({ mod }, 3, function(c)
            c:activate { context = "mouse_click", action = "mouse_resize"}
        end),

    })
end)

-- Keys

awful.keyboard.append_global_keybindings({

	-- Awesome

	awful.key(
		{ mod, "Shift" }, "r", 
			awesome.restart,
		{ description = "reload awesome", group = "awesome" }
	),
	awful.key(
		{ mod }, "z", function() 
			awful.layout.inc(1) 
		end,
 		{ description = "next layout", group = "awesome" }
	),
	awful.key(
		{ mod, "Shift" }, "z", function() 
			awful.layout.inc(-1) 
		end,
 		{ description = "previous layout", group = "awesome" }
	),
    awful.key(
		{ mod }, "Tab", function() 
			awful.client.focus.byidx(1) 
		end,
        { description = "next window", group = "awesome" }
    ),
    awful.key(
		{ mod, "Shift" }, "Tab", function() 
			awful.client.focus.byidx(-1) 
		end,
		{ description = "previous window", group = "awesome" }
    ),
	awful.key(
		{ mod }, "space", function() 
			awesome.emit_signal("widget::menu") 
		end,
		{ description = "show menu", group = "awesome" }
	),
    awful.key(
		{ mod }, "d", function() 
			awesome.emit_signal("widget::launcher")
		end,
        { description = "show launcher", group = "awesome" }
	),
    awful.key(
		{ mod, "Shift" }, "c", function() 
			awesome.emit_signal("widget::config")
		end,
        { description = "show config", group = "awesome" }
	),
    awful.key(
		{ mod }, "BackSpace", function() 
			awesome.emit_signal("widget::lockscreen")
		end,
        { description = "lock screen", group = "awesome" }
	),
    awful.key(
		{ mod }, "space", function() 
			awesome.emit_signal("widget::control")
		end,
        { description = "control center", group = "awesome" }
	),

	-- Programs

	awful.key(
		{ mod }, "Return", function() 
			awful.spawn.with_shell(user.terminal) 
		end,
        { description = "open a terminal", group = "programs" }
	),

	-- Screenshot

	awful.key(
		{ mod }, "Print", function() 
			awesome.emit_signal("util::screenshot", { args = "-u", time = "0" }) 
		end,
        { description = "full screen", group = "screenshot" }
	),
	awful.key(
		{ mod, "Control" }, "Print", function()
			awesome.emit_signal("util::screenshot", { args = "-u", time = "5" }) 
		end,
        { description = "full screen delay", group = "screenshot" }
	),
	awful.key(
		{ mod, "Shift" }, "Print", function() 
			awesome.emit_signal("util::screenshot", { args = "-s -u", time = "0" }) 
		end,
        { description = "part screen", group = "screenshot" }
	),

	-- Volume

    awful.key(
		{ }, "XF86AudioRaiseVolume", function() 
			awful.spawn.with_shell("wpctl set-mute @DEFAULT_AUDIO_SINK@ 0")
			awful.spawn.with_shell("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+")
			awesome.emit_signal("widget::volume")
		end,
        { description = "raise volume", group = "volume" }
	),
    awful.key(
		{ }, "XF86AudioLowerVolume", function() 
			awful.spawn.with_shell("wpctl set-mute @DEFAULT_AUDIO_SINK@ 0")
			awful.spawn.with_shell("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-")
			awesome.emit_signal("widget::volume")
		end,
        { description = "lower volume", group = "volume" }
	),
    awful.key(
		{ }, "XF86AudioMute", function() 
			awful.spawn.with_shell("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle")
			awesome.emit_signal("widget::volume")
		end,
        { description = "mute volume", group = "volume" }
	),

	-- Brightness

	awful.key(
		{ }, "XF86MonBrightnessUp", function() 
			awful.spawn.with_shell("brightnessctl s 5%+")
			awesome.emit_signal("widget::brightness")
		end,
        { description = "raise brightness", group = "brightness" }
	),
	awful.key(
		{ }, "XF86MonBrightnessDown", function() 
			awful.spawn.with_shell("brightnessctl s 5%-")
			awesome.emit_signal("widget::brightness")
		end,
        { description = "lower brightness", group = "brightness" }
	),

	-- Tag

    awful.key {
        modifiers   = { mod },
        keygroup    = "numrow",
        description = "only view tag",
        group       = "tag",
        on_press    = function(index)
            local screen = awful.screen.focused()
            local tag = screen.tags[index]
            if tag then
                tag:view_only()
            end
        end,
    },
    awful.key {
        modifiers = { mod, "Control" },
        keygroup    = "numrow",
        description = "move focused client to tag",
        group       = "tag",
        on_press    = function(index)
            if client.focus then
                local tag = client.focus.screen.tags[index]
                if tag then
                    client.focus:move_to_tag(tag)
                end
            end
        end,
    },
    awful.key {
        modifiers = { mod, "Shift" },
        keygroup    = "numrow",
        description = "move focused client to tag and follow",
        group       = "tag",
        on_press    = function(index)
            if client.focus then
                local tag = client.focus.screen.tags[index]
                if tag then
                    client.focus:move_to_tag(tag)
					tag:view_only()
                end
            end
        end,
    }

})

client.connect_signal("request::default_keybindings", function()
    awful.keyboard.append_client_keybindings({

		-- Client
	
		awful.key(
			{ mod }, "c", 
			function(c) 
				awful.placement.centered(c, { honor_workarea = true }) 
			end,
			{ description = "center window", group = "client" }
		),
        awful.key(
			{ mod }, "f",
            function(c)
                c.fullscreen = not c.fullscreen
                c:raise()
            end,
            { description = "toggle fullscreen", group = "client" }
		),
	    awful.key(
			{ mod }, "s", 
			function(c)
				c.floating = not c.floating
				c:raise()
			end,
        	{ description = "toggle floating", group = "client" }
		),
	    awful.key(
			{ mod }, "n", 
			function(c)
				client.focus.minimized = true
			end,
        	{ description = "minimize", group = "client" }
		),
	    awful.key(
			{ mod }, "m", 
			function(c)
				c.maximized = not c.maximized
				c:raise()
			end,
        	{ description = "toggle maximize", group = "client" }
		),
	    awful.key(
			{ mod }, "b", 
			function(c)
				c.sticky = not c.sticky
				c.above = not c.above
				c:raise()
			end,
        	{ description = "toggle sticky", group = "client" }
		),
		awful.key(
			{ mod, "Shift" }, "q", function(c) 
				c:kill() 
			end,
 			{ description = "close", group = "client" }
		),

    })
end)


================================================
FILE: src/usr/share/calla/desktop/config/init.lua
================================================
require("config.main")
require("config.bind")
require("config.rule")
require("config.shot")


================================================
FILE: src/usr/share/calla/desktop/config/main.lua
================================================
local awful = require("awful")
local gears = require("gears")
local beautiful = require("beautiful")

-- Manage new windows

client.connect_signal('manage', function(c)

	-- Put new windows in stack

	if not awesome.startup then awful.client.setslave(c) end
	if awesome.startup and not c.size_hints.user_position and not c.size_hints.program_position then
		awful.placement.no_offscreen(c)
	end

	if c.x == 0 and c.y == 0 then
		awful.placement.centered(c)
	end

	-- Default icon

	local cairo = require("lgi").cairo
	local defaulticon = "/usr/share/icons/" .. beautiful.icons .. "/64x64/apps/application-default-icon.svg"
	if c and c.valid and not c.icon then
		local s = gears.surface(defaulticon)
		local img = cairo.ImageSurface.create(cairo.Format.ARGB32, s:get_width(), s:get_height())
		local cr = cairo.Context(img)
		cr:set_source_surface(s, 0, 0)
		cr:paint()
		c.icon = img._native
	end

end)

-- Sloppy focus

client.connect_signal("mouse::enter", function(c)
    c:activate { context = "mouse_enter", raise = false }
end)

-- Layouts and tag table

screen.connect_signal("request::desktop_decoration", function(s)
	tag.connect_signal("request::default_layouts", function()
    	awful.layout.append_default_layouts({
			awful.layout.suit.tile,
			awful.layout.suit.floating,
    	})
	end)

    awful.tag({ "1", "2", "3" }, s, awful.layout.layouts[1])
end)

-- Touchpad gestures

awesome.connect_signal("touchpad::gesture", function(direction)
	require("naughty").notification{text="test"}
	if direction == "left" then
		awful.tag.viewprev()
	elseif direction == "right" then
		awful.tag.viewnext()
	elseif direction == "up" then
		awesome.emit_signal("widget::preview")
	elseif direction == "down" then
		awesome.emit_signal("widget::preview:hide")
	end
end)


================================================
FILE: src/usr/share/calla/desktop/config/rule.lua
================================================
local awful = require("awful")
local gears = require("gears")
local ruled = require("ruled")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi

ruled.client.connect_signal("request::rules", function()

	-- New clients

    ruled.client.append_rule {
        id         = "global",
        rule       = { },
        properties = {
            focus     = awful.client.focus.filter,
            raise     = true,
            screen    = awful.screen.preferred,
			placement = awful.placement.no_offscreen
		}
    }

    -- Floating clients

    ruled.client.append_rule {
        id       = "floating",
        rule_any = {
            instance = { "copyq", "pinentry" },
            class    = {
                "Arandr", "Gcolor3", "Blueberry.py", "SimpleScreenRecorder", "Usbimager", "Yad", "Settings"
            },
            name    = {
                "Event Tester",  -- xev
				"ncmpcpp"
            },
        },
        properties = { floating = true }
    }

    -- Titlebars

    ruled.client.append_rule {
        id         = "titlebars",
        rule_any   = { type = { "normal", "dialog" } },
        properties = { titlebars_enabled = true }
    }

end)


================================================
FILE: src/usr/share/calla/desktop/config/shot.lua
================================================
local awful = require("awful")
local naughty = require("naughty")

local function screenshot(args)

	local tmp = "/tmp/" .. os.date("%F-%H%M%S") .. ".png"
	
	awful.spawn.easy_async_with_shell("sleep " .. args.time .. " && maim " .. args.args .. " " .. tmp, function()
		awful.spawn.easy_async_with_shell("[ -e '" .. tmp .. "' ] && echo exists", function(output)
			if output:match('%w+') then
				--[[awful.spawn.easy_async_with_shell("convert -trim " .. tmp .. " " .. tmp, function()
					awful.spawn.with_shell("cat " .. tmp .. " | xclip -se c -t image/png -i")
					awful.spawn.with_shell("cp " .. tmp .. " " .. user.shotdir)
					awful.spawn.with_shell("rm " .. tmp)
				end)--]]
				awful.spawn.with_shell("cat " .. tmp .. " | xclip -se c -t image/png -i")
				awful.spawn.with_shell("cp " .. tmp .. " " .. user.shotdir)
				awful.spawn.with_shell("rm " .. tmp)
				naughty.notification {
					title = "Screenshot",
					text = "Saved to " .. user.shotdir
				}
			else
				naughty.notification {
					title = "Screenshot",
					text = "Cancelled"
				}
			end
		end)
	end)

end

awesome.connect_signal("util::screenshot", function(args)
	screenshot(args)
end)


================================================
FILE: src/usr/share/calla/desktop/json.lua
================================================
-- -*- coding: utf-8 -*-
--
-- Simple JSON encoding and decoding in pure Lua.
--
-- Copyright 2010-2017 Jeffrey Friedl
-- http://regex.info/blog/
-- Latest version: http://regex.info/blog/lua/json
--
-- This code is released under a Creative Commons CC-BY "Attribution" License:
-- http://creativecommons.org/licenses/by/3.0/deed.en_US
--
-- It can be used for any purpose so long as:
--    1) the copyright notice above is maintained
--    2) the web-page links above are maintained
--    3) the 'AUTHOR_NOTE' string below is maintained
--
local VERSION = '20211016.28' -- version history at end of file
local AUTHOR_NOTE = "-[ JSON.lua package by Jeffrey Friedl (http://regex.info/blog/lua/json) version 20211016.28 ]-"

--
-- The 'AUTHOR_NOTE' variable exists so that information about the source
-- of the package is maintained even in compiled versions. It's also
-- included in OBJDEF below mostly to quiet warnings about unused variables.
--
local OBJDEF = {
   VERSION      = VERSION,
   AUTHOR_NOTE  = AUTHOR_NOTE,
}


--
-- Simple JSON encoding and decoding in pure Lua.
-- JSON definition: http://www.json.org/
--
--
--   JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines
--
--   local lua_value = JSON:decode(raw_json_text)
--
--   local raw_json_text    = JSON:encode(lua_table_or_value)
--   local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability
--
--
--
-- DECODING (from a JSON string to a Lua table)
--
--
--   JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines
--
--   local lua_value = JSON:decode(raw_json_text)
--
--   If the JSON text is for an object or an array, e.g.
--     { "what": "books", "count": 3 }
--   or
--     [ "Larry", "Curly", "Moe" ]
--
--   the result is a Lua table, e.g.
--     { what = "books", count = 3 }
--   or
--     { "Larry", "Curly", "Moe" }
--
--
--   The encode and decode routines accept an optional second argument,
--   "etc", which is not used during encoding or decoding, but upon error
--   is passed along to error handlers. It can be of any type (including nil).
--
--
--
-- ERROR HANDLING DURING DECODE
--
--   With most errors during decoding, this code calls
--
--      JSON:onDecodeError(message, text, location, etc)
--
--   with a message about the error, and if known, the JSON text being
--   parsed and the byte count where the problem was discovered. You can
--   replace the default JSON:onDecodeError() with your own function.
--
--   The default onDecodeError() merely augments the message with data
--   about the text and the location (and, an 'etc' argument had been
--   provided to decode(), its value is tacked onto the message as well),
--   and then calls JSON.assert(), which itself defaults to Lua's built-in
--   assert(), and can also be overridden.
--
--   For example, in an Adobe Lightroom plugin, you might use something like
--
--          function JSON:onDecodeError(message, text, location, etc)
--             LrErrors.throwUserError("Internal Error: invalid JSON data")
--          end
--
--   or even just
--
--          function JSON.assert(message)
--             LrErrors.throwUserError("Internal Error: " .. message)
--          end
--
--   If JSON:decode() is passed a nil, this is called instead:
--
--      JSON:onDecodeOfNilError(message, nil, nil, etc)
--
--   and if JSON:decode() is passed HTML instead of JSON, this is called:
--
--      JSON:onDecodeOfHTMLError(message, text, nil, etc)
--
--   The use of the 'etc' argument allows stronger coordination between
--   decoding and error reporting, especially when you provide your own
--   error-handling routines. Continuing with the the Adobe Lightroom
--   plugin example:
--
--          function JSON:onDecodeError(message, text, location, etc)
--             local note = "Internal Error: invalid JSON data"
--             if type(etc) = 'table' and etc.photo then
--                note = note .. " while processing for " .. etc.photo:getFormattedMetadata('fileName')
--             end
--             LrErrors.throwUserError(note)
--          end
--
--            :
--            :
--
--          for i, photo in ipairs(photosToProcess) do
--               :             
--               :             
--               local data = JSON:decode(someJsonText, { photo = photo })
--               :             
--               :             
--          end
--
--
--
--   If the JSON text passed to decode() has trailing garbage (e.g. as with the JSON "[123]xyzzy"),
--   the method
--
--       JSON:onTrailingGarbage(json_text, location, parsed_value, etc)
--
--   is invoked, where:
--
--       'json_text' is the original JSON text being parsed,
--       'location' is the count of bytes into 'json_text' where the garbage starts (6 in the example),
--       'parsed_value' is the Lua result of what was successfully parsed ({123} in the example),
--       'etc' is as above.
--
--   If JSON:onTrailingGarbage() does not abort, it should return the value decode() should return,
--   or nil + an error message.
--
--     local new_value, error_message = JSON:onTrailingGarbage()
--
--   The default JSON:onTrailingGarbage() simply invokes JSON:onDecodeError("trailing garbage"...),
--   but you can have this package ignore trailing garbage via
--
--      function JSON:onTrailingGarbage(json_text, location, parsed_value, etc)
--         return parsed_value
--      end
--
--
-- DECODING AND STRICT TYPES
--
--   Because both JSON objects and JSON arrays are converted to Lua tables,
--   it's not normally possible to tell which original JSON type a
--   particular Lua table was derived from, or guarantee decode-encode
--   round-trip equivalency.
--
--   However, if you enable strictTypes, e.g.
--
--      JSON = assert(loadfile "JSON.lua")() --load the routines
--      JSON.strictTypes = true
--
--   then the Lua table resulting from the decoding of a JSON object or
--   JSON array is marked via Lua metatable, so that when re-encoded with
--   JSON:encode() it ends up as the appropriate JSON type.
--
--   (This is not the default because other routines may not work well with
--   tables that have a metatable set, for example, Lightroom API calls.)
--
--
-- DECODING AND STRICT PARSING
--
--   If strictParsing is true in your JSON object, or if you set strictParsing as a decode option,
--   some kinds of technically-invalid JSON that would normally be accepted are rejected with an error.
--
--   For example, passing in an empty string
--
--      JSON:decode("")
--
--   normally succeeds with a return value of nil, but
--
--      JSON:decode("", nil, { strictParsing = true })
--
--   results in an error being raised (onDecodeError is called).
--
--      JSON.strictParsing = true
--      JSON:decode("")
--
--   achieves the same thing.
--
--
--
-- ENCODING (from a lua table to a JSON string)
--
--   JSON = assert(loadfile "JSON.lua")() -- one-time load of the routines
--
--   local raw_json_text    = JSON:encode(lua_table_or_value)
--   local pretty_json_text = JSON:encode_pretty(lua_table_or_value) -- "pretty printed" version for human readability
--   local custom_pretty    = JSON:encode(lua_table_or_value, etc, { pretty = true, indent = "|  ", align_keys = false })
--
--   On error during encoding, this code calls:
--
--     JSON:onEncodeError(message, etc)
--
--   which you can override in your local JSON object. Also see "HANDLING UNSUPPORTED VALUE TYPES" below.
--
--   The 'etc' in the error call is the second argument to encode() and encode_pretty(), or nil if it wasn't provided.
--
--
--
--
-- ENCODING OPTIONS
--
--   An optional third argument, a table of options, can be provided to encode().
--
--       encode_options =  {
--           -- options for making "pretty" human-readable JSON (see "PRETTY-PRINTING" below)
--           pretty         = true,   -- turn pretty formatting on
--           indent         = "   ",  -- use this indent for each level of an array/object
--           align_keys     = false,  -- if true, align the keys in a way that sounds like it should be nice, but is actually ugly
--           array_newline  = false,  -- if true, array elements become one to a line rather than inline
--           
--           -- other output-related options
--           null           = "\0",   -- see "ENCODING JSON NULL VALUES" below
--           stringsAreUtf8 = false,  -- see "HANDLING UNICODE LINE AND PARAGRAPH SEPARATORS FOR JAVA" below
--       }
--  
--       json_string = JSON:encode(mytable, etc, encode_options)
--
--
--
-- For reference, the defaults are:
--
--           pretty         = false
--           null           = nil,
--           stringsAreUtf8 = false,
--
--
--
-- PRETTY-PRINTING
--
--   Enabling the 'pretty' encode option helps generate human-readable JSON.
--
--     pretty = JSON:encode(val, etc, {
--                                       pretty = true,
--                                       indent = "   ",
--                                       align_keys = false,
--                                     })
--
--   encode_pretty() is also provided: it's identical to encode() except
--   that encode_pretty() provides a default options table if none given in the call:
--
--       { pretty = true, indent = "  ", align_keys = false, array_newline = false }
--
--   For example, if
--
--      JSON:encode(data)
--
--   produces:
--
--      {"city":"Kyoto","climate":{"avg_temp":16,"humidity":"high","snowfall":"minimal"},"country":"Japan","wards":11}
--
--   then
--
--      JSON:encode_pretty(data)
--
--   produces:
--
--      {
--        "city": "Kyoto",
--        "climate": {
--          "avg_temp": 16,
--          "humidity": "high",
--          "snowfall": "minimal"
--        },
--        "country": "Japan",
--        "wards": 11
--      }
--
--   The following lines all return identical strings:
--       JSON:encode_pretty(data)
--       JSON:encode_pretty(data, nil, { pretty = true, indent = "  ", align_keys = false, array_newline = false})
--       JSON:encode_pretty(data, nil, { pretty = true, indent = "  " })
--       JSON:encode       (data, nil, { pretty = true, indent = "  " })
--
--   An example of setting your own indent string:
--
--     JSON:encode_pretty(data, nil, { pretty = true, indent = "|    " })
--
--   produces:
--
--      {
--      |    "city": "Kyoto",
--      |    "climate": {
--      |    |    "avg_temp": 16,
--      |    |    "humidity": "high",
--      |    |    "snowfall": "minimal"
--      |    },
--      |    "country": "Japan",
--      |    "wards": 11
--      }
--
--   An example of setting align_keys to true:
--
--     JSON:encode_pretty(data, nil, { pretty = true, indent = "  ", align_keys = true })
--  
--   produces:
--   
--      {
--           "city": "Kyoto",
--        "climate": {
--                     "avg_temp": 16,
--                     "humidity": "high",
--                     "snowfall": "minimal"
--                   },
--        "country": "Japan",
--          "wards": 11
--      }
--
--   which I must admit is kinda ugly, sorry. This was the default for
--   encode_pretty() prior to version 20141223.14.
--
--
--  HANDLING UNICODE LINE AND PARAGRAPH SEPARATORS FOR JAVA
--
--    If the 'stringsAreUtf8' encode option is set to true, consider Lua strings not as a sequence of bytes,
--    but as a sequence of UTF-8 characters.
--
--    Currently, the only practical effect of setting this option is that Unicode LINE and PARAGRAPH
--    separators, if found in a string, are encoded with a JSON escape instead of being dumped as is.
--    The JSON is valid either way, but encoding this way, apparently, allows the resulting JSON
--    to also be valid Java.
--
--  AMBIGUOUS SITUATIONS DURING THE ENCODING
--
--   During the encode, if a Lua table being encoded contains both string
--   and numeric keys, it fits neither JSON's idea of an object, nor its
--   idea of an array. To get around this, when any string key exists (or
--   when non-positive numeric keys exist), numeric keys are converted to
--   strings.
--
--   For example, 
--     JSON:encode({ "one", "two", "three", SOMESTRING = "some string" }))
--   produces the JSON object
--     {"1":"one","2":"two","3":"three","SOMESTRING":"some string"}
--
--   To prohibit this conversion and instead make it an error condition, set
--      JSON.noKeyConversion = true
--
--
-- ENCODING JSON NULL VALUES
--
--   Lua tables completely omit keys whose value is nil, so without special handling there's
--   no way to represent JSON object's null value in a Lua table.  For example
--      JSON:encode({ username = "admin", password = nil })
--
--   produces:
--
--      {"username":"admin"}
--
--   In order to actually produce
--
--      {"username":"admin", "password":null}
--

--   one can include a string value for a "null" field in the options table passed to encode().... 
--   any Lua table entry with that value becomes null in the JSON output:
--
--      JSON:encode({ username = "admin", password = "xyzzy" }, -- First arg is the Lua table to encode as JSON.
--                  nil,                                        -- Second arg is the 'etc' value, ignored here
--                  { null = "xyzzy" })                         -- Third arg is th options table
--
--   produces:
--
--      {"username":"admin", "password":null}
--
--   Just be sure to use a string that is otherwise unlikely to appear in your data.
--   The string "\0" (a string with one null byte) may well be appropriate for many applications.
--
--   The "null" options also applies to Lua tables that become JSON arrays.
--      JSON:encode({ "one", "two", nil, nil })
--
--   produces
--
--      ["one","two"]
--
--   while
--
--      NullPlaceholder = "\0"
--      encode_options = { null = NullPlaceholder }
--      JSON:encode({ "one", "two", NullPlaceholder, NullPlaceholder}, nil, encode_options)
--   produces
--
--      ["one","two",null,null]
--
--
--
-- HANDLING LARGE AND/OR PRECISE NUMBERS
--
--
--   Without special handling, numbers in JSON can lose precision in Lua.
--   For example:
--   
--      T = JSON:decode('{  "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345  }')
--
--      print("small:   ",  type(T.small),    T.small)
--      print("big:     ",  type(T.big),      T.big)
--      print("precise: ",  type(T.precise),  T.precise)
--   
--   produces
--   
--      small:          number  12345
--      big:            number  1.2345678901235e+28
--      precise:        number  9876.6789012346
--
--   Precision is lost with both 'big' and 'precise'.
--
--   This package offers ways to try to handle this better (for some definitions of "better")...
--
--   The most precise method is by setting the global:
--   
--      JSON.decodeNumbersAsObjects = true
--   
--   When this is set, numeric JSON data is encoded into Lua in a form that preserves the exact
--   JSON numeric presentation when re-encoded back out to JSON, or accessed in Lua as a string.
--
--   This is done by encoding the numeric data with a Lua table/metatable that returns
--   the possibly-imprecise numeric form when accessed numerically, but the original precise
--   representation when accessed as a string.
--
--   Consider the example above, with this option turned on:
--
--      JSON.decodeNumbersAsObjects = true
--      
--      T = JSON:decode('{  "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345  }')
--
--      print("small:   ",  type(T.small),    T.small)
--      print("big:     ",  type(T.big),      T.big)
--      print("precise: ",  type(T.precise),  T.precise)
--   
--   This now produces:
--   
--      small:          table   12345
--      big:            table   12345678901234567890123456789
--      precise:        table   9876.67890123456789012345
--   
--   However, within Lua you can still use the values (e.g. T.precise in the example above) in numeric
--   contexts. In such cases you'll get the possibly-imprecise numeric version, but in string contexts
--   and when the data finds its way to this package's encode() function, the original full-precision
--   representation is used.
--
--   You can force access to the string or numeric version via
--        JSON:forceString()
--        JSON:forceNumber()
--   For example,
--        local probably_okay = JSON:forceNumber(T.small) -- 'probably_okay' is a number
--
--   Code the inspects the JSON-turned-Lua data using type() can run into troubles because what used to
--   be a number can now be a table (e.g. as the small/big/precise example above shows). Update these
--   situations to use JSON:isNumber(item), which returns nil if the item is neither a number nor one
--   of these number objects. If it is either, it returns the number itself. For completeness there's
--   also JSON:isString(item).
--
--   If you want to try to avoid the hassles of this "number as an object" kludge for all but really
--   big numbers, you can set JSON.decodeNumbersAsObjects and then also set one or both of
--            JSON:decodeIntegerObjectificationLength
--            JSON:decodeDecimalObjectificationLength
--   They refer to the length of the part of the number before and after a decimal point. If they are
--   set and their part is at least that number of digits, objectification occurs. If both are set,
--   objectification occurs when either length is met.
--
--   -----------------------
--
--   Even without using the JSON.decodeNumbersAsObjects option, you can encode numbers in your Lua
--   table that retain high precision upon encoding to JSON, by using the JSON:asNumber() function:
--
--      T = {
--         imprecise =                123456789123456789.123456789123456789,
--         precise   = JSON:asNumber("123456789123456789.123456789123456789")
--      }
--
--      print(JSON:encode_pretty(T))
--
--   This produces:
--
--      { 
--         "precise": 123456789123456789.123456789123456789,
--         "imprecise": 1.2345678912346e+17
--      }
--
--
--   -----------------------
--
--   A different way to handle big/precise JSON numbers is to have decode() merely return the exact
--   string representation of the number instead of the number itself. This approach might be useful
--   when the numbers are merely some kind of opaque object identifier and you want to work with them
--   in Lua as strings anyway.
--   
--   This approach is enabled by setting
--
--      JSON.decodeIntegerStringificationLength = 10
--
--   The value is the number of digits (of the integer part of the number) at which to stringify numbers.
--   NOTE: this setting is ignored if JSON.decodeNumbersAsObjects is true, as that takes precedence.
--
--   Consider our previous example with this option set to 10:
--
--      JSON.decodeIntegerStringificationLength = 10
--      
--      T = JSON:decode('{  "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345  }')
--
--      print("small:   ",  type(T.small),    T.small)
--      print("big:     ",  type(T.big),      T.big)
--      print("precise: ",  type(T.precise),  T.precise)
--
--   This produces:
--
--      small:          number  12345
--      big:            string  12345678901234567890123456789
--      precise:        number  9876.6789012346
--
--   The long integer of the 'big' field is at least JSON.decodeIntegerStringificationLength digits
--   in length, so it's converted not to a Lua integer but to a Lua string. Using a value of 0 or 1 ensures
--   that all JSON numeric data becomes strings in Lua.
--
--   Note that unlike
--      JSON.decodeNumbersAsObjects = true
--   this stringification is simple and unintelligent: the JSON number simply becomes a Lua string, and that's the end of it.
--   If the string is then converted back to JSON, it's still a string. After running the code above, adding
--      print(JSON:encode(T))
--   produces
--      {"big":"12345678901234567890123456789","precise":9876.6789012346,"small":12345}
--   which is unlikely to be desired.
--
--   There's a comparable option for the length of the decimal part of a number:
--
--      JSON.decodeDecimalStringificationLength
--
--   This can be used alone or in conjunction with
--
--      JSON.decodeIntegerStringificationLength
--
--   to trip stringification on precise numbers with at least JSON.decodeIntegerStringificationLength digits after
--   the decimal point. (Both are ignored if JSON.decodeNumbersAsObjects is true.)
--
--   This example:
--
--      JSON.decodeIntegerStringificationLength = 10
--      JSON.decodeDecimalStringificationLength =  5
--
--      T = JSON:decode('{  "small":12345, "big":12345678901234567890123456789, "precise":9876.67890123456789012345  }')
--      
--      print("small:   ",  type(T.small),    T.small)
--      print("big:     ",  type(T.big),      T.big)
--      print("precise: ",  type(T.precise),  T.precise)
--
--  produces:
--
--      small:          number  12345
--      big:            string  12345678901234567890123456789
--      precise:        string  9876.67890123456789012345
--
--
--  HANDLING UNSUPPORTED VALUE TYPES
--
--   Among the encoding errors that might be raised is an attempt to convert a table value that has a type
--   that this package hasn't accounted for: a function, userdata, or a thread. You can handle these types as table
--   values (but not as table keys) if you supply a JSON:unsupportedTypeEncoder() method along the lines of the
--   following example:
--        
--        function JSON:unsupportedTypeEncoder(value_of_unsupported_type)
--           if type(value_of_unsupported_type) == 'function' then
--              return "a function value"
--           else
--              return nil
--           end
--        end
--        
--   Your unsupportedTypeEncoder() method is actually called with a bunch of arguments:
--
--      self:unsupportedTypeEncoder(value, parents, etc, options, indent, for_key)
--
--   The 'value' is the function, thread, or userdata to be converted to JSON.
--
--   The 'etc' and 'options' arguments are those passed to the original encode(). The other arguments are
--   probably of little interest; see the source code. (Note that 'for_key' is never true, as this function
--   is invoked only on table values; table keys of these types still trigger the onEncodeError method.)
--
--   If your unsupportedTypeEncoder() method returns a string, it's inserted into the JSON as is.
--   If it returns nil plus an error message, that error message is passed through to an onEncodeError invocation.
--   If it returns only nil, processing falls through to a default onEncodeError invocation.
--
--   If you want to handle everything in a simple way:
--
--        function JSON:unsupportedTypeEncoder(value)
--           return tostring(value)
--        end
--
--
-- SUMMARY OF METHODS YOU CAN OVERRIDE IN YOUR LOCAL LUA JSON OBJECT
--
--    assert
--    onDecodeError
--    onDecodeOfNilError
--    onDecodeOfHTMLError
--    onTrailingGarbage
--    onEncodeError
--    unsupportedTypeEncoder
--
--  If you want to create a separate Lua JSON object with its own error handlers,
--  you can reload JSON.lua or use the :new() method.
--
---------------------------------------------------------------------------

local default_pretty_indent  = "  "
local default_pretty_options = { pretty = true, indent = default_pretty_indent, align_keys = false, array_newline = false }

local isArray  = { __tostring = function() return "JSON array"         end }  isArray.__index  = isArray
local isObject = { __tostring = function() return "JSON object"        end }  isObject.__index = isObject

function OBJDEF:newArray(tbl)
   return setmetatable(tbl or {}, isArray)
end

function OBJDEF:newObject(tbl)
   return setmetatable(tbl or {}, isObject)
end




local function getnum(op)
   return type(op) == 'number' and op or op.N
end

local isNumber = {
   __tostring = function(T)  return T.S        end,
   __unm      = function(op) return getnum(op) end,

   __concat   = function(op1, op2) return tostring(op1) .. tostring(op2) end,
   __add      = function(op1, op2) return getnum(op1)   +   getnum(op2)  end,
   __sub      = function(op1, op2) return getnum(op1)   -   getnum(op2)  end,
   __mul      = function(op1, op2) return getnum(op1)   *   getnum(op2)  end,
   __div      = function(op1, op2) return getnum(op1)   /   getnum(op2)  end,
   __mod      = function(op1, op2) return getnum(op1)   %   getnum(op2)  end,
   __pow      = function(op1, op2) return getnum(op1)   ^   getnum(op2)  end,
   __lt       = function(op1, op2) return getnum(op1)   <   getnum(op2)  end,
   __eq       = function(op1, op2) return getnum(op1)   ==  getnum(op2)  end,
   __le       = function(op1, op2) return getnum(op1)   <=  getnum(op2)  end,
}
isNumber.__index = isNumber

function OBJDEF:asNumber(item)

   if getmetatable(item) == isNumber then
      -- it's already a JSON number object.
      return item
   elseif type(item) == 'table' and type(item.S) == 'string' and type(item.N) == 'number' then
      -- it's a number-object table that lost its metatable, so give it one
      return setmetatable(item, isNumber)
   else
      -- the normal situation... given a number or a string representation of a number....
      local holder = {
         S = tostring(item), -- S is the representation of the number as a string, which remains precise
         N = tonumber(item), -- N is the number as a Lua number.
      }
      return setmetatable(holder, isNumber)
   end
end

--
-- Given an item that might be a normal string or number, or might be an 'isNumber' object defined above,
-- return the string version. This shouldn't be needed often because the 'isNumber' object should autoconvert
-- to a string in most cases, but it's here to allow it to be forced when needed.
--
function OBJDEF:forceString(item)
   if type(item) == 'table' and type(item.S) == 'string' then
      return item.S
   else
      return tostring(item)
   end
end

--
-- Given an item that might be a normal string or number, or might be an 'isNumber' object defined above,
-- return the numeric version.
--
function OBJDEF:forceNumber(item)
   if type(item) == 'table' and type(item.N) == 'number' then
      return item.N
   else
      return tonumber(item)
   end
end

--
-- If the given item is a number, return it. Otherwise, return nil.
-- This, this can be used both in a conditional and to access the number when you're not sure its form.
--
function OBJDEF:isNumber(item)
   if type(item) == 'number' then
      return item
   elseif type(item) == 'table' and type(item.N) == 'number' then
      return item.N
   else
      return nil
   end
end

function OBJDEF:isString(item)
   if type(item) == 'string' then
      return item
   elseif type(item) == 'table' and type(item.S) == 'string' then
      return item.S
   else
      return nil
   end
end




--
-- Some utf8 routines to deal with the fact that Lua handles only bytes
--
local function top_three_bits(val)
   return math.floor(val / 0x20)
end

local function top_four_bits(val)
   return math.floor(val / 0x10)
end

local function unicode_character_bytecount_based_on_first_byte(first_byte)
   local W = string.byte(first_byte)
   if W < 0x80 then
      return 1
   elseif (W == 0xC0) or (W == 0xC1) or (W >= 0x80 and W <= 0xBF) or (W >= 0xF5) then
      -- this is an error -- W can't be the start of a utf8 character
      return 0
   elseif top_three_bits(W) == 0x06 then
      return 2
   elseif top_four_bits(W) == 0x0E then
      return 3
   else
      return 4
   end
end



local function unicode_codepoint_as_utf8(codepoint)
   --
   -- codepoint is a number
   --
   if codepoint <= 127 then
      return string.char(codepoint)

   elseif codepoint <= 2047 then
      --
      -- 110yyyxx 10xxxxxx         <-- useful notation from http://en.wikipedia.org/wiki/Utf8
      --
      local highpart = math.floor(codepoint / 0x40)
      local lowpart  = codepoint - (0x40 * highpart)
      return string.char(0xC0 + highpart,
                         0x80 + lowpart)

   elseif codepoint <= 65535 then
      --
      -- 1110yyyy 10yyyyxx 10xxxxxx
      --
      local highpart  = math.floor(codepoint / 0x1000)
      local remainder = codepoint - 0x1000 * highpart
      local midpart   = math.floor(remainder / 0x40)
      local lowpart   = remainder - 0x40 * midpart

      highpart = 0xE0 + highpart
      midpart  = 0x80 + midpart
      lowpart  = 0x80 + lowpart

      --
      -- Check for an invalid character (thanks Andy R. at Adobe).
      -- See table 3.7, page 93, in http://www.unicode.org/versions/Unicode5.2.0/ch03.pdf#G28070
      --
      if ( highpart == 0xE0 and midpart < 0xA0 ) or
         ( highpart == 0xED and midpart > 0x9F ) or
         ( highpart == 0xF0 and midpart < 0x90 ) or
         ( highpart == 0xF4 and midpart > 0x8F )
      then
         return "?"
      else
         return string.char(highpart,
                            midpart,
                            lowpart)
      end

   else
      --
      -- 11110zzz 10zzyyyy 10yyyyxx 10xxxxxx
      --
      local highpart  = math.floor(codepoint / 0x40000)
      local remainder = codepoint - 0x40000 * highpart
      local midA      = math.floor(remainder / 0x1000)
      remainder       = remainder - 0x1000 * midA
      local midB      = math.floor(remainder / 0x40)
      local lowpart   = remainder - 0x40 * midB

      return string.char(0xF0 + highpart,
                         0x80 + midA,
                         0x80 + midB,
                         0x80 + lowpart)
   end
end

function OBJDEF:onDecodeError(message, text, location, etc)
   if text then
      if location then
         message = string.format("%s at byte %d of: %s", message, location, text)
      else
         message = string.format("%s: %s", message, text)
      end
   end

   if etc ~= nil then
      message = message .. " (" .. OBJDEF:encode(etc) .. ")"
   end

   if self.assert then
      self.assert(false, message)
   else
      assert(false, message)
   end
end

function OBJDEF:onTrailingGarbage(json_text, location, parsed_value, etc)
   return self:onDecodeError("trailing garbage", json_text, location, etc)
end

OBJDEF.onDecodeOfNilError  = OBJDEF.onDecodeError
OBJDEF.onDecodeOfHTMLError = OBJDEF.onDecodeError

function OBJDEF:onEncodeError(message, etc)
   if etc ~= nil then
      message = message .. " (" .. OBJDEF:encode(etc) .. ")"
   end

   if self.assert then
      self.assert(false, message)
   else
      assert(false, message)
   end
end

local function grok_number(self, text, start, options)
   --
   -- Grab the integer part
   --
   local integer_part = text:match('^-?[1-9]%d*', start)
                     or text:match("^-?0",        start)

   if not integer_part then
      self:onDecodeError("expected number", text, start, options.etc)
      return nil, start -- in case the error method doesn't abort, return something sensible
   end

   local i = start + integer_part:len()

   --
   -- Grab an optional decimal part
   --
   local decimal_part = text:match('^%.%d+', i) or ""

   i = i + decimal_part:len()

   --
   -- Grab an optional exponential part
   --
   local exponent_part = text:match('^[eE][-+]?%d+', i) or ""

   i = i + exponent_part:len()

   local full_number_text = integer_part .. decimal_part .. exponent_part

   if options.decodeNumbersAsObjects then

      local objectify = false

      if not options.decodeIntegerObjectificationLength and not options.decodeDecimalObjectificationLength then
         -- no options, so objectify
         objectify = true

      elseif (options.decodeIntegerObjectificationLength
          and
         (integer_part:len() >= options.decodeIntegerObjectificationLength or exponent_part:len() > 0))

          or
         (options.decodeDecimalObjectificationLength 
          and
          (decimal_part:len() >= options.decodeDecimalObjectificationLength  or exponent_part:len() > 0))
      then
         -- have options and they are triggered, so objectify
         objectify = true
      end

      if objectify then
         return OBJDEF:asNumber(full_number_text), i
      end
      -- else, fall through to try to return as a straight-up number

   else

      -- Not always decoding numbers as objects, so perhaps encode as strings?

      --
      -- If we're told to stringify only under certain conditions, so do.
      -- We punt a bit when there's an exponent by just stringifying no matter what.
      -- I suppose we should really look to see whether the exponent is actually big enough one
      -- way or the other to trip stringification, but I'll be lazy about it until someone asks.
      --
      if (options.decodeIntegerStringificationLength
          and
         (integer_part:len() >= options.decodeIntegerStringificationLength or exponent_part:len() > 0))

          or

         (options.decodeDecimalStringificationLength 
          and
          (decimal_part:len() >= options.decodeDecimalStringificationLength or exponent_part:len() > 0))
      then
         return full_number_text, i -- this returns the exact string representation seen in the original JSON
      end

   end


   local as_number = tonumber(full_number_text)

   if not as_number then
      self:onDecodeError("bad number", text, start, options.etc)
      return nil, start -- in case the error method doesn't abort, return something sensible
   end

   return as_number, i
end


local backslash_escape_conversion = {
   ['"'] = '"',
   ['/'] = "/",
   ['\\'] = "\\",
   ['b'] = "\b",
   ['f'] = "\f",
   ['n'] = "\n",
   ['r'] = "\r",
   ['t'] = "\t",
}

local function grok_string(self, text, start, options)

   if text:sub(start,start) ~= '"' then
      self:onDecodeError("expected string's opening quote", text, start, options.etc)
      return nil, start -- in case the error method doesn't abort, return something sensible
   end

   local i = start + 1 -- +1 to bypass the initial quote
   local text_len = text:len()
   local VALUE = ""
   while i <= text_len do
      local c = text:sub(i,i)
      if c == '"' then
         return VALUE, i + 1
      end
      if c ~= '\\' then
         
         -- should grab the next bytes as per the number of bytes for this utf8 character
         local byte_count = unicode_character_bytecount_based_on_first_byte(c)

         local next_character
         if byte_count == 0 then
            self:onDecodeError("non-utf8 sequence", text, i, options.etc)
         elseif byte_count == 1 then
            if options.strictParsing and string.byte(c) < 0x20 then
               self:onDecodeError("Unescaped control character", text, i+1, options.etc)
               return nil, start -- in case the error method doesn't abort, return something sensible
            end
            next_character = c
         elseif byte_count == 2 then
            next_character = text:match('^(.[\128-\191])', i)
         elseif byte_count == 3 then
            next_character = text:match('^(.[\128-\191][\128-\191])', i)
         elseif byte_count == 4 then
            next_character = text:match('^(.[\128-\191][\128-\191][\128-\191])', i)
         end

         if not next_character then
            self:onDecodeError("incomplete utf8 sequence", text, i, options.etc) 
            return nil, i -- in case the error method doesn't abort, return something sensible           
         end


         VALUE = VALUE .. next_character
         i = i + byte_count

      else
         --
         -- We have a backslash escape
         --
         i = i + 1

         local next_byte = text:match('^(.)', i)

         if next_byte == nil then
            -- string ended after the \ 
            self:onDecodeError("unfinished \\ escape", text, i, options.etc)
            return nil, start -- in case the error method doesn't abort, return something sensible
         end

         if backslash_escape_conversion[next_byte] then
            VALUE = VALUE .. backslash_escape_conversion[next_byte]
            i = i + 1
         else
            --
            -- The only other valid use of \ that remains is in the form of \u####
            --

            local hex = text:match('^u([0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)
            if hex then
               i = i + 5 -- bypass what we just read

               -- We have a Unicode codepoint. It could be standalone, or if in the proper range and
               -- followed by another in a specific range, it'll be a two-code surrogate pair.
               local codepoint = tonumber(hex, 16)
               if codepoint >= 0xD800 and codepoint <= 0xDBFF then
                  -- it's a hi surrogate... see whether we have a following low
                  local lo_surrogate = text:match('^\\u([dD][cdefCDEF][0123456789aAbBcCdDeEfF][0123456789aAbBcCdDeEfF])', i)
                  if lo_surrogate then
                     i = i + 6 -- bypass the low surrogate we just read
                     codepoint = 0x2400 + (codepoint - 0xD800) * 0x400 + tonumber(lo_surrogate, 16)
                  else
                     -- not a proper low, so we'll just leave the first codepoint as is and spit it out.
                  end
               end
               VALUE = VALUE .. unicode_codepoint_as_utf8(codepoint)

            elseif options.strictParsing then
               --local next_byte = text:match('^\\(.)', i) printf("NEXT[%s]", next_byte);
               self:onDecodeError("illegal use of backslash escape", text, i, options.etc)
               return nil, start -- in case the error method doesn't abort, return something sensible
            else
               local byte_count = unicode_character_bytecount_based_on_first_byte(next_byte)
               if byte_count == 0 then
                  self:onDecodeError("non-utf8 sequence after backslash escape", text, i, options.etc)
                  return nil, start -- in case the error method doesn't abort, return something sensible
               end

               local next_character
               if byte_count == 1 then
                  next_character = next_byte
               elseif byte_count == 2 then
                  next_character = text:match('^(.[\128-\191])', i)
               elseif byte_count == 3 then
                  next_character = text:match('^(.[\128-\191][\128-\191])', i)
               elseif byte_count == 3 then
                  next_character = text:match('^(.[\128-\191][\128-\191][\128-\191])', i)
               end

               if next_character == nil then
                  -- incomplete utf8 character after escape
                  self:onDecodeError("incomplete utf8 sequence after backslash escape", text, i, options.etc)
                  return nil, start -- in case the error method doesn't abort, return something sensible
               end

               VALUE = VALUE .. next_character
               i = i + byte_count
            end
         end
      end
   end

   self:onDecodeError("unclosed string", text, start, options.etc)
   return nil, start -- in case the error method doesn't abort, return something sensible
end

local function skip_whitespace(text, start)

   local _, match_end = text:find("^[ \n\r\t]+", start) -- [ https://datatracker.ietf.org/doc/html/rfc7158#section-2 ]
   if match_end then
      return match_end + 1
   else
      return start
   end
end

local grok_one -- assigned later

local function grok_object(self, text, start, options)

   if text:sub(start,start) ~= '{' then
      self:onDecodeError("expected '{'", text, start, options.etc)
      return nil, start -- in case the error method doesn't abort, return something sensible
   end

   local i = skip_whitespace(text, start + 1) -- +1 to skip the '{'

   local VALUE = self.strictTypes and self:newObject { } or { }

   if text:sub(i,i) == '}' then
      return VALUE, i + 1
   end
   local text_len = text:len()
   while i <= text_len do
      local key, new_i = grok_string(self, text, i, options)

      i = skip_whitespace(text, new_i)

      if text:sub(i, i) ~= ':' then
         self:onDecodeError("expected colon", text, i, options.etc)
         return nil, i -- in case the error method doesn't abort, return something sensible
      end

      i = skip_whitespace(text, i + 1)

      local new_val, new_i = grok_one(self, text, i, options)

      VALUE[key] = new_val

      --
      -- Expect now either '}' to end things, or a ',' to allow us to continue.
      --
      i = skip_whitespace(text, new_i)

      local c = text:sub(i,i)

      if c == '}' then
         return VALUE, i + 1
      end

      if text:sub(i, i) ~= ',' then
         self:onDecodeError("expected comma or '}'", text, i, options.etc)
         return nil, i -- in case the error method doesn't abort, return something sensible
      end

      i = skip_whitespace(text, i + 1)
   end

   self:onDecodeError("unclosed '{'", text, start, options.etc)
   return nil, start -- in case the error method doesn't abort, return something sensible
end

local function grok_array(self, text, start, options)
   if text:sub(start,start) ~= '[' then
      self:onDecodeError("expected '['", text, start, options.etc)
      return nil, start -- in case the error method doesn't abort, return something sensible
   end

   local i = skip_whitespace(text, start + 1) -- +1 to skip the '['
   local VALUE = self.strictTypes and self:newArray { } or { }
   if text:sub(i,i) == ']' then
      return VALUE, i + 1
   end

   local VALUE_INDEX = 1

   local text_len = text:len()
   while i <= text_len do
      local val, new_i = grok_one(self, text, i, options)

      -- can't table.insert(VALUE, val) here because it's a no-op if val is nil
      VALUE[VALUE_INDEX] = val
      VALUE_INDEX = VALUE_INDEX + 1

      i = skip_whitespace(text, new_i)

      --
      -- Expect now either ']' to end things, or a ',' to allow us to continue.
      --
      local c = text:sub(i,i)
      if c == ']' then
         return VALUE, i + 1
      end
      if text:sub(i, i) ~= ',' then
         self:onDecodeError("expected comma or ']'", text, i, options.etc)
         return nil, i -- in case the error method doesn't abort, return something sensible
      end
      i = skip_whitespace(text, i + 1)
   end
   self:onDecodeError("unclosed '['", text, start, options.etc)
   return nil, i -- in case the error method doesn't abort, return something sensible
end


grok_one = function(self, text, start, options)
   -- Skip any whitespace
   start = skip_whitespace(text, start)

   if start > text:len() then
      self:onDecodeError("unexpected end of string", text, nil, options.etc)
      return nil, start -- in case the error method doesn't abort, return something sensible
   end

   if text:find('^"', start) then
      return grok_string(self, text, start, options)

   elseif text:find('^[-0123456789 ]', start) then
      return grok_number(self, text, start, options)

   elseif text:find('^%{', start) then
      return grok_object(self, text, start, options)

   elseif text:find('^%[', start) then
      return grok_array(self, text, start, options)

   elseif text:find('^true', start) then
      return true, start + 4

   elseif text:find('^false', start) then
      return false, start + 5

   elseif text:find('^null', start) then
      return options.null, start + 4

   else
      self:onDecodeError("can't parse JSON", text, start, options.etc)
      return nil, 1 -- in case the error method doesn't abort, return something sensible
   end
end

function OBJDEF:decode(text, etc, options)
   --
   -- If the user didn't pass in a table of decode options, make an empty one.
   --
   if type(options) ~= 'table' then
      options = {}
   end

   --
   -- If they passed in an 'etc' argument, stuff it into the options.
   -- (If not, any 'etc' field in the options they passed in remains to be used)
   --
   if etc ~= nil then
      options.etc = etc
   end


   --
   -- apply global options
   --
   if options.decodeNumbersAsObjects == nil then
      options.decodeNumbersAsObjects = self.decodeNumbersAsObjects
   end
   if options.decodeIntegerObjectificationLength == nil then
      options.decodeIntegerObjectificationLength = self.decodeIntegerObjectificationLength
   end
   if options.decodeDecimalObjectificationLength == nil then
      options.decodeDecimalObjectificationLength = self.decodeDecimalObjectificationLength
   end
   if options.decodeIntegerStringificationLength == nil then
      options.decodeIntegerStringificationLength = self.decodeIntegerStringificationLength
   end
   if options.decodeDecimalStringificationLength == nil then
      options.decodeDecimalStringificationLength = self.decodeDecimalStringificationLength
   end
   if options.strictParsing == nil then
      options.strictParsing = self.strictParsing
   end


   if type(self) ~= 'table' or self.__index ~= OBJDEF then
      local error_message = "JSON:decode must be called in method format"
      OBJDEF:onDecodeError(error_message, nil, nil, options.etc)
      return nil, error_message -- in case the error method doesn't abort, return something sensible
   end

   if text == nil then
      local error_message = "nil passed to JSON:decode()"
      self:onDecodeOfNilError(error_message, nil, nil, options.etc)
      return nil, error_message -- in case the error method doesn't abort, return something sensible

   elseif type(text) ~= 'string' then
      local error_message = "expected string argument to JSON:decode()"
      self:onDecodeError(string.format("%s, got %s", error_message, type(text)), nil, nil, options.etc)
      return nil, error_message -- in case the error method doesn't abort, return something sensible
   end

   -- If passed an empty string....
   if text:match('^%s*$') then
      if options.strictParsing then
         local error_message = "empty string passed to JSON:decode()"
         self:onDecodeOfNilError(error_message, nil, nil, options.etc)
         return nil, error_message -- in case the error method doesn't abort, return something sensible
      else
         -- we'll consider it nothing, but not an error
         return nil
      end
   end

   if text:match('^%s*<') then
      -- Can't be JSON... we'll assume it's HTML
      local error_message = "HTML passed to JSON:decode()"
      self:onDecodeOfHTMLError(error_message, text, nil, options.etc)
      return nil, error_message -- in case the error method doesn't abort, return something sensible
   end

   --
   -- Ensure that it's not UTF-32 or UTF-16.
   -- Those are perfectly valid encodings for JSON (as per RFC 4627 section 3),
   -- but this package can't handle them.
   --
   if text:sub(1,1):byte() == 0 or (text:len() >= 2 and text:sub(2,2):byte() == 0) then
      local error_message = "JSON package groks only UTF-8, sorry"
      self:onDecodeError(error_message, text, nil, options.etc)
      return nil, error_message -- in case the error method doesn't abort, return something sensible
   end


   --
   -- Finally, go parse it
   --
   local success, value, next_i = pcall(grok_one, self, text, 1, options)

   if success then

      local error_message = nil
      if next_i ~= #text + 1 then
         -- something's left over after we parsed the first thing.... whitespace is allowed.
         next_i = skip_whitespace(text, next_i)

         -- if we have something left over now, it's trailing garbage
         if next_i ~= #text + 1 then
            value, error_message = self:onTrailingGarbage(text, next_i, value, options.etc)
         end
      end
      return value, error_message

   else

      -- If JSON:onDecodeError() didn't abort out of the pcall, we'll have received
      -- the error message here as "value", so pass it along as an assert.
      local error_message = value
      if self.assert then
         self.assert(false, error_message)
      else
         assert(false, error_message)
      end
      -- ...and if we're still here (because the assert didn't throw an error),
      -- return a nil and throw the error message on as a second arg
      return nil, error_message

   end
end

local function backslash_replacement_function(c)
   if     c == "\n" then     return "\\n"
   elseif c == "\r" then     return "\\r"
   elseif c == "\t" then     return "\\t"
   elseif c == "\b" then     return "\\b"
   elseif c == "\f" then     return "\\f"
   elseif c == '"' then      return '\\"'
   elseif c == '\\' then     return '\\\\'
   elseif c == '/' then      return '/'
   else
      return string.format("\\u%04x", c:byte())
   end
end

local chars_to_be_escaped_in_JSON_string
   = '['
   ..    '"'    -- class sub-pattern to match a double quote
   ..    '%\\'  -- class sub-pattern to match a backslash
   ..    '/'    -- class sub-pattern to match a forwardslash
   ..    '%z'   -- class sub-pattern to match a null
   ..    '\001' .. '-' .. '\031' -- class sub-pattern to match control characters
   .. ']'


local LINE_SEPARATOR_as_utf8      = unicode_codepoint_as_utf8(0x2028)
local PARAGRAPH_SEPARATOR_as_utf8 = unicode_codepoint_as_utf8(0x2029)
local function json_string_literal(value, options)
   local newval = value:gsub(chars_to_be_escaped_in_JSON_string, backslash_replacement_function)
   if options.stringsAreUtf8 then
      --
      -- This feels really ugly to just look into a string for the sequence of bytes that we know to be a particular utf8 character,
      -- but utf8 was designed purposefully to make this kind of thing possible. Still, feels dirty.
      -- I'd rather decode the byte stream into a character stream, but it's not technically needed so
      -- not technically worth it.
      --
      newval = newval:gsub(LINE_SEPARATOR_as_utf8, '\\u2028'):gsub(PARAGRAPH_SEPARATOR_as_utf8,'\\u2029')
   end
   return '"' .. newval .. '"'
end

local function object_or_array(self, T, etc)
   --
   -- We need to inspect all the keys... if there are any strings, we'll convert to a JSON
   -- object. If there are only numbers, it's a JSON array.
   --
   -- If we'll be converting to a JSON object, we'll want to sort the keys so that the
   -- end result is deterministic.
   --
   local string_keys = { }
   local number_keys = { }
   local number_keys_must_be_strings = false
   local maximum_number_key

   for key in pairs(T) do
      if type(key) == 'string' then
         table.insert(string_keys, key)
      elseif type(key) == 'number' then
         table.insert(number_keys, key)
         if key <= 0 or key >= math.huge then
            number_keys_must_be_strings = true
         elseif not maximum_number_key or key > maximum_number_key then
            maximum_number_key = key
         end
      elseif type(key) == 'boolean' then
         table.insert(string_keys, tostring(key))
      else
         self:onEncodeError("can't encode table with a key of type " .. type(key), etc)
      end
   end

   if #string_keys == 0 and not number_keys_must_be_strings then
      --
      -- An empty table, or a numeric-only array
      --
      if #number_keys > 0 then
         return nil, maximum_number_key -- an array
      elseif tostring(T) == "JSON array" then
         return nil
      elseif tostring(T) == "JSON object" then
         return { }
      else
         -- have to guess, so we'll pick array, since empty arrays are likely more common than empty objects
         return nil
      end
   end

   table.sort(string_keys)

   local map
   if #number_keys > 0 then
      --
      -- If we're here then we have either mixed string/number keys, or numbers inappropriate for a JSON array
      -- It's not ideal, but we'll turn the numbers into strings so that we can at least create a JSON object.
      --

      if self.noKeyConversion then
         self:onEncodeError("a table with both numeric and string keys could be an object or array; aborting", etc)
      end

      --
      -- Have to make a shallow copy of the source table so we can remap the numeric keys to be strings
      --
      map = { }
      for key, val in pairs(T) do
         map[key] = val
      end

      table.sort(number_keys)

      --
      -- Throw numeric keys in there as strings
      --
      for _, number_key in ipairs(number_keys) do
         local string_key = tostring(number_key)
         if map[string_key] == nil then
            table.insert(string_keys , string_key)
            map[string_key] = T[number_key]
         else
            self:onEncodeError("conflict converting table with mixed-type keys into a JSON object: key " .. number_key .. " exists both as a string and a number.", etc)
         end
      end
   end

   return string_keys, nil, map
end

--
-- Encode
--
-- 'options' is nil, or a table with possible keys:
--
--    pretty         -- If true, return a pretty-printed version.
--
--    indent         -- A string (usually of spaces) used to indent each nested level.
--
--    align_keys     -- If true, align all the keys when formatting a table. The result is uglier than one might at first imagine.
--                      Results are undefined if 'align_keys' is true but 'pretty' is not.
--
--    array_newline  -- If true, array elements are formatted each to their own line. The default is to all fall inline.
--                      Results are undefined if 'array_newline' is true but 'pretty' is not.
--
--    null           -- If this exists with a string value, table elements with this value are output as JSON null.
--
--    stringsAreUtf8 -- If true, consider Lua strings not as a sequence of bytes, but as a sequence of UTF-8 characters.
--                      (Currently, the only practical effect of setting this option is that Unicode LINE and PARAGRAPH
--                       separators, if found in a string, are encoded with a JSON escape instead of as raw UTF-8.
--                       The JSON is valid either way, but encoding this way, apparently, allows the resulting JSON
--                       to also be valid Java.)
--
--
local function encode_value(self, value, parents, etc, options, indent, for_key)

   --
   -- keys in a JSON object can never be null, so we don't even consider options.null when converting a key value
   --
   if value == nil or (not for_key and options and options.null and value == options.null) then
      return 'null'

   elseif type(value) == 'string' then
      return json_string_literal(value, options)

   elseif type(value) == 'number' then
      if value ~= value then
         --
         -- NaN (Not a Number).
         -- JSON has no NaN, so we have to fudge the best we can. This should really be a package option.
         --
         return "null"
      elseif value >= math.huge then
         --
         -- Positive infinity. JSON has no INF, so we have to fudge the best we can. This should
         -- really be a package option. Note: at least with some implementations, positive infinity
         -- is both ">= math.huge" and "<= -math.huge", which makes no sense but that's how it is.
         -- Negative infinity is properly "<= -math.huge". So, we must be sure to check the ">="
         -- case first.
         --
         return "1e+9999"
      elseif value <= -math.huge then
         --
         -- Negative infinity.
         -- JSON has no INF, so we have to fudge the best we can. This should really be a package option.
         --
         return "-1e+9999"
      else
         return tostring(value)
      end

   elseif type(value) == 'boolean' then
      return tostring(value)

   elseif type(value) ~= 'table' then

      if self.unsupportedTypeEncoder then
         local user_value, user_error = self:unsupportedTypeEncoder(value, parents, etc, options, indent, for_key)
         -- If the user's handler returns a string, use that. If it returns nil plus an error message, bail with that.
         -- If only nil returned, fall through to the default error handler.
         if type(user_value) == 'string' then
            return user_value
         elseif user_value ~= nil then
            self:onEncodeError("unsupportedTypeEncoder method returned a " .. type(user_value), etc)
         elseif user_error then
            self:onEncodeError(tostring(user_error), etc)
         end
      end

      self:onEncodeError("can't convert " .. type(value) .. " to JSON", etc)

   elseif getmetatable(value) == isNumber then
      return tostring(value)
   else
      --
      -- A table to be converted to either a JSON object or array.
      --
      local T = value

      if type(options) ~= 'table' then
         options = {}
      end
      if type(indent) ~= 'string' then
         indent = ""
      end

      if parents[T] then
         self:onEncodeError("table " .. tostring(T) .. " is a child of itself", etc)
      else
         parents[T] = true
      end

      local result_value

      local object_keys, maximum_number_key, map = object_or_array(self, T, etc)
      if maximum_number_key then
         --
         -- An array...
         --
         local key_indent
         if options.array_newline then
            key_indent = indent .. tostring(options.indent or "")
         else
            key_indent = indent
         end

         local ITEMS = { }
         for i = 1, maximum_number_key do
            table.insert(ITEMS, encode_value(self, T[i], parents, etc, options, key_indent))
         end

         if options.array_newline then
            result_value = "[\n" .. key_indent .. table.concat(ITEMS, ",\n" .. key_indent) .. "\n" .. indent .. "]"
         elseif options.pretty then
            result_value = "[ " .. table.concat(ITEMS, ", ") .. " ]"
         else
            result_value = "["  .. table.concat(ITEMS, ",")  .. "]"
         end

      elseif object_keys then
         --
         -- An object
         --
         local TT = map or T

         if options.pretty then

            local KEYS = { }
            local max_key_length = 0
            for _, key in ipairs(object_keys) do
               local encoded = encode_value(self, tostring(key), parents, etc, options, indent, true)
               if options.align_keys then
                  max_key_length = math.max(max_key_length, #encoded)
               end
               table.insert(KEYS, encoded)
            end
            local key_indent = indent .. tostring(options.indent or "")
            local subtable_indent = key_indent .. string.rep(" ", max_key_length) .. (options.align_keys and "  " or "")
            local FORMAT = "%s%s: %s"

            local COMBINED_PARTS = { }
            for i, key in ipairs(object_keys) do
               local encoded_val = encode_value(self, TT[key], parents, etc, options, subtable_indent)
               table.insert(COMBINED_PARTS, string.format(FORMAT, key_indent, KEYS[i], encoded_val))
            end
            result_value = "{\n" .. table.concat(COMBINED_PARTS, ",\n") .. "\n" .. indent .. "}"

         else

            local PARTS = { }
            for _, key in ipairs(object_keys) do
               local encoded_val = encode_value(self, TT[key],       parents, etc, options, indent)
               local encoded_key = encode_value(self, tostring(key), parents, etc, options, indent, true)
               table.insert(PARTS, string.format("%s:%s", encoded_key, encoded_val))
            end
            result_value = "{" .. table.concat(PARTS, ",") .. "}"

         end
      else
         --
         -- An empty array/object... we'll treat it as an array, though it should really be an option
         --
         result_value = "[]"
      end

      parents[T] = false
      return result_value
   end
end

local function top_level_encode(self, value, etc, options)
   local val = encode_value(self, value, {}, etc, options)
   if val == nil then
      --PRIVATE("may need to revert to the previous public verison if I can't figure out what the guy wanted")
      return val
   else
      return val
   end
end

function OBJDEF:encode(value, etc, options)
   if type(self) ~= 'table' or self.__index ~= OBJDEF then
      OBJDEF:onEncodeError("JSON:encode must be called in method format", etc)
   end

   --
   -- If the user didn't pass in a table of decode options, make an empty one.
   --
   if type(options) ~= 'table' then
      options = {}
   end

   return top_level_encode(self, value, etc, options)
end

function OBJDEF:encode_pretty(value, etc, options)
   if type(self) ~= 'table' or self.__index ~= OBJDEF then
      OBJDEF:onEncodeError("JSON:encode_pretty must be called in method format", etc)
   end

   --
   -- If the user didn't pass in a table of decode options, use the default pretty ones
   --
   if type(options) ~= 'table' then
      options = default_pretty_options
   end

   return top_level_encode(self, value, etc, options)
end

function OBJDEF.__tostring()
   return "JSON encode/decode package"
end

OBJDEF.__index = OBJDEF

function OBJDEF:new(args)
   local new = { }

   if args then
      for key, val in pairs(args) do
         new[key] = val
      end
   end

   return setmetatable(new, OBJDEF)
end

return OBJDEF:new()

--
-- Version history:
--
--   20211016.28   Had forgotten to document the strictParsing option.
--
--   20211015.27   Better handle some edge-case errors [ thank you http://seriot.ch/projects/parsing_json.html ; all tests are now successful ]
--
--                 Added some semblance of proper UTF8 parsing, and now aborts with an error on ilformatted UTF8.
--
--                 Added the strictParsing option:
--                    Aborts with an error on unknown backslash-escape in strings
--                    Aborts on naked control characters in strings
--                    Aborts when decode is passed a whitespace-only string
--
--                 For completeness, when encoding a Lua string into a JSON string, escape a forward slash.
--
--                 String decoding should be a bit more efficient now.
--
--   20170927.26   Use option.null in decoding as well. Thanks to Max Sindwani for the bump, and sorry to Oliver Hitz
--                 whose first mention of it four years ago was completely missed by me.
--
--   20170823.25   Added support for JSON:unsupportedTypeEncoder().
--                 Thanks to Chronos Phaenon Eosphoros (https://github.com/cpeosphoros) for the idea.
--
--   20170819.24   Added support for boolean keys in tables.
--
--   20170416.23   Added the "array_newline" formatting option suggested by yurenchen (http://www.yurenchen.com/)
--
--   20161128.22   Added:
--                   JSON:isString()
--                   JSON:isNumber()
--                   JSON:decodeIntegerObjectificationLength
--                   JSON:decodeDecimalObjectificationLength
--
--   20161109.21   Oops, had a small boo-boo in the previous update.
--
--   20161103.20   Used to silently ignore trailing garbage when decoding. Now fails via JSON:onTrailingGarbage()
--                 http://seriot.ch/parsing_json.php
--
--                 Built-in error message about "expected comma or ']'" had mistakenly referred to '['
--
--                 Updated the built-in error reporting to refer to bytes rather than characters.
--
--                 The decode() method no longer assumes that error handlers abort.
--
--                 Made the VERSION string a string instead of a number
--

--   20160916.19   Fixed the isNumber.__index assignment (thanks to Jack Taylor)
--   
--   20160730.18   Added JSON:forceString() and JSON:forceNumber()
--
--   20160728.17   Added concatenation to the metatable for JSON:asNumber()
--
--   20160709.16   Could crash if not passed an options table (thanks jarno heikkinen <jarnoh@capturemonkey.com>).
--
--                 Made JSON:asNumber() a bit more resilient to being passed the results of itself.
--
--   20160526.15   Added the ability to easily encode null values in JSON, via the new "null" encoding option.
--                 (Thanks to Adam B for bringing up the issue.)
--
--                 Added some support for very large numbers and precise floats via
--                    JSON.decodeNumbersAsObjects
--                    JSON.decodeIntegerStringificationLength
--                    JSON.decodeDecimalStringificationLength
--
--                 Added the "stringsAreUtf8" encoding option. (Hat tip to http://lua-users.org/wiki/JsonModules )
--
--   20141223.14   The encode_pretty() routine produced fine results for small datasets, but isn't really
--                 appropriate for anything large, so with help from Alex Aulbach I've made the encode routines
--                 more flexible, and changed the default encode_pretty() to be more generally useful.
--
--                 Added a third 'options' argument to the encode() and encode_pretty() routines, to control
--                 how the encoding takes place.
--
--                 Updated docs to add assert() call to the loadfile() line, just as good practice so that
--                 if there is a problem loading JSON.lua, the appropriate error message will percolate up.
--
--   20140920.13   Put back (in a way that doesn't cause warnings about unused variables) the author string,
--                 so that the source of the package, and its version number, are visible in compiled copies.
--
--   20140911.12   Minor lua cleanup.
--                 Fixed internal reference to 'JSON.noKeyConversion' to reference 'self' instead of 'JSON'.
--                 (Thanks to SmugMug's David Parry for these.)
--
--   20140418.11   JSON nulls embedded within an array were being ignored, such that
--                     ["1",null,null,null,null,null,"seven"],
--                 would return
--                     {1,"seven"}
--                 It's now fixed to properly return
--                     {1, nil, nil, nil, nil, nil, "seven"}
--                 Thanks to "haddock" for catching the error.
--
--   20140116.10   The user's JSON.assert() wasn't always being used. Thanks to "blue" for the heads up.
--
--   20131118.9    Update for Lua 5.3... it seems that tostring(2/1) produces "2.0" instead of "2",
--                 and this caused some problems.
--
--   20131031.8    Unified the code for encode() and encode_pretty(); they had been stupidly separate,
--                 and had of course diverged (encode_pretty didn't get the fixes that encode got, so
--                 sometimes produced incorrect results; thanks to Mattie for the heads up).
--
--                 Handle encoding tables with non-positive numeric keys (unlikely, but possible).
--
--                 If a table has both numeric and string keys, or its numeric keys are inappropriate
--                 (such as being non-positive or infinite), the numeric keys are turned into
--                 string keys appropriate for a JSON object. So, as before,
--                         JSON:encode({ "one", "two", "three" })
--                 produces the array
--                         ["one","two","three"]
--                 but now something with mixed key types like
--                         JSON:encode({ "one", "two", "three", SOMESTRING = "some string" }))
--                 instead of throwing an error produces an object:
--                         {"1":"one","2":"two","3":"three","SOMESTRING":"some string"}
--
--                 To maintain the prior throw-an-error semantics, set
--                      JSON.noKeyConversion = true
--                 
--   20131004.7    Release under a Creative Commons CC-BY license, which I should have done from day one, sorry.
--
--   20130120.6    Comment update: added a link to the specific page on my blog where this code can
--                 be found, so that folks who come across the code outside of my blog can find updates
--                 more easily.
--
--   20111207.5    Added support for the 'etc' arguments, for better error reporting.
--
--   20110731.4    More feedback from David Kolf on how to make the tests for Nan/Infinity system independent.
--
--   20110730.3    Incorporated feedback from David Kolf at http://lua-users.org/wiki/JsonModules:
--
--                   * When encoding lua for JSON, Sparse numeric arrays are now handled by
--                     spitting out full arrays, such that
--                        JSON:encode({"one", "two", [10] = "ten"})
--                     returns
--                        ["one","two",null,null,null,null,null,null,null,"ten"]
--
--                     In 20100810.2 and earlier, only up to the first non-null value would have been retained.
--
--                   * When encoding lua for JSON, numeric value NaN gets spit out as null, and infinity as "1+e9999".
--                     Version 20100810.2 and earlier created invalid JSON in both cases.
--
--                   * Unicode surrogate pairs are now detected when decoding JSON.
--
--   20100810.2    added some checking to ensure that an invalid Unicode character couldn't leak in to the UTF-8 encoding
--
--   20100731.1    initial public release
--


================================================
FILE: src/usr/share/calla/desktop/rc.lua
================================================
--[[ 
--	TODO
--
--	Why is text content changed when its color is changed? -- markdown? needs to be bg container?
--
--	Features:
--	Alt+tab menu
--	Desktop menu
--
--	Settings:
--	Completely redesign settings app
--	Add autostart commands to settings
--	Add modkey/sessionlock to settings
--	Add profile picture to settings
--	Create Gtk theme from color json
--	Import theme (Xresources?)
--
--	Design:
--	Redesign preview to look more like macos (expose)
--	Hover cursor on titlebar buttons
--	Hover background?
--
--	Refactor:
--	Is it finally time to make a helpers file...?
--	Make battery use upower
--	Move custom themes to cache
--	Get rid of color dir
--	Differentiate custom themes from default
--
--	Long Term:
--	Better multihead support
--]]

--[[
--	Known Bugs
--
--	Multihead:
--	Location of lockscreen promptbox depends on focused screen at startup,
--	doesn't appear if laptop screen focused
--	Systray opens/closes for both screens, one is redundant
--
--	General:
--	Preview does not entirely reload if visible
--	Desktop get grid function does not account for spacing
--	Icon theme is not refreshed with live reload
--	fprintd-verify does not work after suspend (issue #173)
--
--	Unknown Bugs:
--	Many
--]]

local awful = require("awful")
local gears = require("gears")
local naughty = require("naughty")

-- Errors

naughty.connect_signal("request::display_error", function(message, startup)
	naughty.notification {
		urgency = "critical",
		title   = "Error"..(startup and " during startup!" or "!"),
		message = message
	}
end)

-- Json

function readjson(path)
	local r = assert(io.open(path, "r"))
	local table = r:read("*all")
	r:close()
	table = require("json"):decode(table)
	return table
end

function writejson(path, table)
	local w = assert(io.open(path, "w"))
	w:write(require("json"):encode_pretty(table, nil, { pretty = true, indent = "	", align_keys = false, array_newline = true}))
	w:close()
end

local config = gears.filesystem.get_cache_dir() .. "user.json"

local defaults = {
	batt = "BAT0",
	color = "light",
	font = "Roboto Medium 11",
	fontalt = "Roboto Bold 11",
	mod = "Mod4",
	passwd = "awesomewm",
	reboot = "systemctl reboot",
	sessionlock = false,
	shotdir = "~/Pictures/Screenshots",
	shutdown = "systemctl poweroff",
	terminal = "st"
}

if not gears.filesystem.file_readable(config) then
	writejson(config, defaults)
end

user = readjson(config)

-- Config

require("awful.autofocus")
require("signal")
require("config")
require("theme")
require("color.desktop")

-- Startup

local autostart = {
	"picom -b --config '/usr/share/calla/compositor.conf'",
	"xsettingsd --config '/usr/share/calla/xsettingsd'",
	"nm-applet",
	"/usr/lib/policykit-1-gnome/polkit-gnome-authentication-agent-1"
}

local function restarted()
	awesome.register_xproperty("restarted", "boolean")
	local detected = awesome.get_xproperty("restarted") ~= nil
	awesome.set_xproperty("restarted", true)
	return detected
end

if not restarted() then
	for _, command in ipairs(autostart) do
		awful.spawn.easy_async({ 'pkill', '--full', '--uid', os.getenv('USER'), '^' .. command }, function()
			awful.spawn.easy_async_with_shell(command, function() end) -- func needed to avoid callback error
		end)
	end
	if user.sessionlock then
		awesome.emit_signal("widget::lockscreen")
	end
end

-- Theme Init

awesome.emit_signal("live::reload")


================================================
FILE: src/usr/share/calla/desktop/signal/brightness.lua
================================================
local awful = require("awful")

local function emit()
	awful.spawn.easy_async_with_shell("brightnessctl -m | awk -F, '{print substr($4, 0, length($4)-1)}'", function(out)
		local brightness = math.floor(tonumber(out))
		awesome.emit_signal("signal::brightness", brightness)
	end)
end

emit()

local subscribe = [[ bash -c "while (inotifywait -e modify /sys/class/backlight/?*/brightness -qq) do echo; done" ]]

awful.spawn.easy_async_with_shell("ps x | grep \"inotifywait -e modify /sys/class/backlight\" | grep -v grep | awk '{print $1}' | xargs kill", function ()
	awful.spawn.with_line_callback(subscribe, {
		stdout = function() emit() end
	})
end)


================================================
FILE: src/usr/share/calla/desktop/signal/desktop.lua
================================================
local awful = require("awful")

local emit = function(type)
	awesome.emit_signal("signal::desktop", type) 
end

emit()

local addsubscribe = [[
   bash -c "
   while inotifywait -e create -e moved_to $HOME/Desktop/ -qq; do echo; done
"]]

local removesubscribe = [[
   bash -c "
   while inotifywait -e delete -e moved_from $HOME/Desktop/ -qq; do echo; done
"]]

awful.spawn.easy_async_with_shell("ps x | grep \"inotifywait -e create -e moved_to $HOME/Desktop/\" | grep -v grep | awk '{print $1}' | xargs kill", function ()
    awful.spawn.with_line_callback(addsubscribe, {
        stdout = function() emit("add") end
    })
end)

awful.spawn.easy_async_with_shell("ps x | grep \"inotifywait -e delete -e moved_from $HOME/Desktop/\" | grep -v grep | awk '{print $1}' | xargs kill", function ()
    awful.spawn.with_line_callback(removesubscribe, {
        stdout = function() emit("remove") end
    })
end)


================================================
FILE: src/usr/share/calla/desktop/signal/init.lua
================================================
require("signal.volume")
require("signal.brightness")
require("signal.playerctl")
require("signal.desktop")


================================================
FILE: src/usr/share/calla/desktop/signal/playerctl.lua
================================================
local awful = require("awful")

local function emit()
	awful.spawn.easy_async_with_shell("playerctl --player=%any,firefox,chromium metadata --format 'title_{{title}}album_{{album}}artist_{{artist}}cover_{{mpris:artUrl}}elapsed_{{duration(position)}}total_{{duration(mpris:length)}}' & playerctl status", function(out)
		local title, album, artist, status

		title = out:match("title_(.*)album_") or "Not Playing"
		album = out:match("album_(.*)artist_") or "No Album"
		artist = out:match("artist_(.*)cover_") or "No Artist"
		cover = out:match("cover_file://(.*)elapsed_") or "None"
		elapsed = out:match("elapsed_(.*)total_") or "0:00"
		total = out:match("total_(.-)\n") or "0:00"

		if out:match("Playing") then
			status = true
		else
			status = false
		end

		awesome.emit_signal('signal::playerctl', title, album, artist, cover, elapsed, total, status)
	end)
end

emit()

local subscribe = [[ bash -c "playerctl metadata --format 'title_{{title}}album_{{album}}artist_{{artist}}cover_{{mpris:artUrl}}elapsed_{{duration(position)}}total_{{duration(mpris:length)}}' -F & playerctl status -F" ]]

awful.spawn.easy_async({ 'pkill', '--full', '--uid', os.getenv('USER'), '^playerctl' }, function()
    awful.spawn.with_line_callback(subscribe, {
        stdout = function() emit() end
    })
end)


================================================
FILE: src/usr/share/calla/desktop/signal/volume.lua
================================================
local awful = require("awful")

local volume_old = -1
local muted_old = -1
local function emit()
	awful.spawn.easy_async_with_shell("wpctl get-volume @DEFAULT_AUDIO_SINK@", function(out)
		local volume = tonumber(string.match(out:match('(%d%.%d+)')*100, '(%d+)'))
		local muted = out:match('MUTED')

		if volume ~= volume_old or muted ~= muted_old then
			awesome.emit_signal('signal::volume', volume, muted)
			volume_old = volume
			muted_old = muted
		end
	end)
end

emit()

local subscribe = [[ bash -c "LANG=C pactl subscribe 2> /dev/null | grep --line-buffered \"Event 'change' on sink\"" ]]

awful.spawn.easy_async({ 'pkill', '--full', '--uid', os.getenv('USER'), '^pactl subscribe' }, function()
	awful.spawn.with_line_callback(subscribe, {
		stdout = function() emit() end
	})
end)


================================================
FILE: src/usr/share/calla/desktop/theme/brightness.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi

local brightnessbox = wibox {
	width = dpi(200),
	height = dpi(85),
	ontop = true,
	visible = false
}

local percent = wibox.widget.textbox()

local header = wibox.widget {
	{
		{
			{
				{
					valign = "center",
					widget = wibox.widget.textbox("Brightness")
				},
				nil,
				percent,
				layout = wibox.layout.align.horizontal,
			},
			top = dpi(5),
			bottom = dpi(5),
			left = dpi(10),
			right = dpi(10),
			widget = wibox.container.margin
		},
		widget = background({ bg = "bgmid" })
	},
	margins = dpi(5),
	widget = wibox.container.margin
}

local icon = iconbox({ image = "brightness0" })

local bar = wibox.widget {
	shape = gears.shape.rounded_rect,
	bar_shape = gears.shape.rounded_rect,
	max_value = 100,
	value = 0,
	widget = live(wibox.widget.progressbar, { background_color = "bgmid", color = "fg" })
}

local timer = gears.timer {
	timeout = 2,
	single_shot = true,
	callback = function()
		brightnessbox.visible = false
	end
}

brightnessbox:setup {
	{
		header,
		{
			{
				{
					icon,
					right = dpi(15),
					widget = wibox.container.margin
				},
				nil,
				{
					bar,
					top = dpi(20),
					bottom = dpi(20),
					widget = wibox.container.margin
				},
				layout = wibox.layout.align.horizontal
			},
			left = dpi(15),
			right = dpi(15),
			widget = wibox.container.margin
		},
		layout = wibox.layout.align.vertical
	},
	widget = live(wibox.container.background, { bg = "bg", fg = "fg" })
}

awesome.connect_signal("signal::brightness", function(brightness)
	percent.text = tostring(brightness) .. "%"
	bar.value = brightness
	if brightness >= 75 then
		icon.image = createicon("brightness100")
	elseif brightness >= 50 then
		icon.image = createicon("brightness75")
	elseif brightness >= 25 then
		icon.image = createicon("brightness50")
	elseif brightness > 0 then
		icon.image = createicon("brightness25")
	elseif brightness == 0 then
		icon.image = createicon("brightness0")
	end
end)

awesome.connect_signal("widget::brightness", function()
	awesome.emit_signal("widget::volume:hide")

	timer:again()

	if client.focus and client.focus.fullscreen == true then
		awful.placement.bottom(
			brightnessbox, 
			{
				margins = { 
					bottom = dpi(10)
				}, 
				parent = awful.screen.focused()
			}
		)
	else
		awful.placement.bottom(
			brightnessbox, 
			{
				margins = { 
					bottom = dpi(60)
				}, 
				parent = awful.screen.focused()
			}
		)
	end

	brightnessbox.visible = true
end)

awesome.connect_signal("widget::brightness:hide", function() 
	brightnessbox.visible = false 
end)


================================================
FILE: src/usr/share/calla/desktop/theme/control/calendar.lua
================================================
local wibox = require("wibox")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi

local function decorate(widget, flag, date)
	local retbg
	if flag == "focus" then retbg = beautiful.fg .. "33" else retbg = beautiful.bgmid end
	local ret = wibox.widget {
		{
			widget,
			halign = "center",
			valign = "center",
			fill_horizontal = true,
			fill_vertical = true,
			widget = wibox.container.place
		},
		bg = retbg,
		widget = background({ fg = "fg" })
	}
	awesome.connect_signal("live::reload", function()
		if flag == "focus" then retbg = beautiful.fg .. "33" else retbg = beautiful.bgmid end
		ret.bg = retbg
	end)
	return ret
end

local calendar = wibox.widget {
	{
		{
			font = user.font,
			start_sunday = true,
			flex_height = true,
			fn_embed = decorate,
			widget = wibox.widget.calendar.month(os.date("*t"))
		},
		margins = dpi(10),
		widget = wibox.container.margin
	},
	forced_width = dpi(220),
	forced_height = dpi(220),
	widget = background({ bg = "bgmid", fg = "fg" })
}

return calendar


================================================
FILE: src/usr/share/calla/desktop/theme/control/init.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi

local media = require("theme.control.media")
local sliders = require("theme.control.sliders")
local toggles = require("theme.control.toggles")
local system = require("theme.control.system")
local profile = require("theme.control.profile")
local calendar = require("theme.control.calendar")
local notifs = require("theme.control.notifs")

local controlbox = wibox {
	width = dpi(470),
	height = dpi(360),
	ontop = true,
	visible = false,
	widget = {
		{
			{
				media,
				{
					sliders,
					toggles,
					system,
					spacing = dpi(10),
					layout = wibox.layout.fixed.vertical
				},
				spacing = dpi(10),
				layout = wibox.layout.fixed.horizontal
			},
			margins = dpi(10),
			widget = wibox.container.margin
		},
		widget = background({ bg = "bg" })
	}
}

local infobox = wibox {
	width = dpi(470),
	height = dpi(360),
	ontop = true,
	visible = false,
	widget = {
		{
			{
				{
					profile,
					calendar,
					spacing = dpi(10),
					layout = wibox.layout.fixed.vertical
				},
				notifs,
				spacing = dpi(10),
				layout = wibox.layout.fixed.horizontal
			},
			margins = dpi(10),
			widget = wibox.container.margin
		},
		widget = background({ bg = "bg" })
	}
}

awesome.connect_signal("widget::control", function()
	controlbox.visible = not controlbox.visible
	infobox.visible = not infobox.visible

	if client.focus and client.focus.fullscreen == true then
		awful.placement.bottom_right(
			controlbox,
			{
				margins = {
					bottom = dpi(10),
					right = dpi(10)
				},
				parent = awful.screen.focused()
			}
		)
		awful.placement.bottom_right(
			infobox,
			{
				margins = {
					bottom = dpi(380),
					right = dpi(10)
				},
				parent = awful.screen.focused()
			}
		)
	else
		awful.placement.bottom_right(
			controlbox,
			{
				margins = {
					bottom = dpi(60),
					right = dpi(20)
				},
				parent = awful.screen.focused()
			}
		)
		awful.placement.bottom_right(
			infobox,
			{
				margins = {
					bottom = dpi(430),
					right = dpi(20)
				},
				parent = awful.screen.focused()
			}
		)
	end
end)



================================================
FILE: src/usr/share/calla/desktop/theme/control/media.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi

local function mediabutton(args)
	return hovercursor(wibox.widget {
		buttons = { awful.button({}, 1, args.run) },
		widget = iconbox({ image = args.icon, size = dpi(24) })
	})
end

local media = wibox.widget {
	{
		{
			{
				{
					{
						id = "cover",
						upscale = false,
						downscale = true,
						valign = "center",
						halign = "center",
						clip_shape = function(cr, width, height)
							gears.shape.rounded_rect(cr, width, height, dpi(5))
						end,
						widget = wibox.widget.imagebox
					},
					widget = background({ bg = "bgalt", fg = "fg" })
				},
				width = dpi(200),
				height = dpi(200),
				strategy = "exact",
				widget = wibox.container.constraint
			},
			nil,
			{
				{
					{
						{
							{
								id = "title",
								align = "center",
								widget = wibox.widget.textbox("Not Playing")
							},
							height = dpi(20),
							widget = wibox.container.constraint
						},
						{
							{
								id = "artist",
								align = "center",
								widget = wibox.widget.textbox("No Artist")
							},
							height = dpi(20),
							widget = wibox.container.constraint
						},
						spacing = dpi(5),
						layout = wibox.layout.fixed.vertical
					},
					align = "center",
					widget = wibox.container.place
				},
				{
					{
						{
							id = "elapsed",
							widget = wibox.widget.textbox("0:00")
						},
						right = dpi(10),
						widget = wibox.container.margin
					},
					{
						{
							id = "progress",
							max_value = 0,
							value = 0,
							bar_shape = gears.shape.rounded_rect,
							shape = gears.shape.rounded_rect,
							forced_height = dpi(5),
							expand = true,
							widget = live(wibox.widget.progressbar, { color = "fg", background_color = "bgalt" })
						},
						forced_height = dpi(10),
						widget = wibox.container.place
					},
					{
						{
							id = "total",
							widget = wibox.widget.textbox("0:00")
						},
						left = dpi(10),
						widget = wibox.container.margin
					},
					layout = wibox.layout.align.horizontal
				},
				{
					{
						mediabutton({ icon = "previous", run = function() awful.spawn.with_shell("playerctl previous") end }),
						{
							id = "pause",
							widget = mediabutton({ icon = "play", run = function() awful.spawn.with_shell("playerctl play-pause") end })
						},
						mediabutton({ icon = "next", run = function() awful.spawn.with_shell("playerctl next") end }),
						spacing = dpi(5),
						layout = wibox.layout.fixed.horizontal
					},
					align = "center",
					widget = wibox.container.place
				},
				spacing = dpi(10),
				layout = wibox.layout.fixed.vertical
			},
			layout = wibox.layout.align.vertical
		},
		margins = dpi(10),
		widget = wibox.container.margin
	},
	forced_width = dpi(220),
	forced_height = dpi(300),
	widget = background({ bg = "bgmid", fg = "fg" })
}

awesome.connect_signal("signal::playerctl", function(title, album, artist, cover, elapsed, total, status)
	media:get_children_by_id("title")[1].text = title
	media:get_children_by_id("artist")[1].text = artist

	local coverart = cover
	if cover == "None" then coverart = beautiful.calla end
	media:get_children_by_id("cover")[1].image = coverart

	if total ~= "" then
		media:get_children_by_id("elapsed")[1].text = elapsed
		media:get_children_by_id("total")[1].text = total
		local elapsedseconds = tonumber(elapsed:match("(.*):"))*60 + tonumber(elapsed:match(":(.*)"))
		local totalseconds = tonumber(total:match("(.*):"))*60 + tonumber(total:match(":(.*)"))
		media:get_children_by_id("progress")[1].value = elapsedseconds
		media:get_children_by_id("progress")[1].max_value = totalseconds
	end

	if status then
		media:get_children_by_id("pause")[1].image = createicon("pause")
	else
		media:get_children_by_id("pause")[1].image = createicon("play")
	end

	awesome.connect_signal("live::reload", function()
		if cover == "None" then coverart = beautiful.calla end
		media:get_children_by_id("cover")[1].image = coverart
	end)
end)

return media


================================================
FILE: src/usr/share/calla/desktop/theme/control/notifs.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local naughty = require("naughty")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi

local notifcontainer = wibox.widget {
	spacing = dpi(10),
	layout = wibox.layout.fixed.vertical
}

local notifempty = wibox.widget {
	wibox.widget.textbox("No Notifications"),
	fill_vertical = true,
	align = "center",
	layout = wibox.container.place
}

local function notifbutton(widget)
	return hovercursor(wibox.widget {
		{
			iconbox({ image = widget.image }),
			margins = dpi(5),
			widget = wibox.container.margin
		},
		widget = background({ bg = "bgalt", fg = "fg" })
	})
end

local function removenotif(notif)
	notifcontainer:remove_widgets(notif)
	if #notifcontainer.children == 0 then
		notifempty.visible = true
	end
end

local function createnotif(n)
	local notif = wibox.widget {
		{
			{
				{
					{
						{
							{
								wibox.widget.textbox(n.title),
								width = dpi(140),
								widget = wibox.container.constraint
							},
							nil,
							{
								{ id = "remove", widget = hovercursor(wibox.widget {
									{
										iconbox({ image = "close" }),
										margins = dpi(5),
										widget = wibox.container.margin
									},
									widget = background({ bg = "bgmid", fg = "fg" })
								})},
								valign = "top",
								widget = wibox.container.place
							},
							layout = wibox.layout.align.horizontal
						},
						left = dpi(10),
						right = dpi(5),
						top = dpi(5),
						bottom = dpi(5),
						widget = wibox.container.margin
					},
					widget = background({ bg = "bgmid", fg = "fg" })
				},
				{
					wibox.widget.textbox(n.message:gsub("'", "\'")),
					margins = dpi(5),
					widget = wibox.container.margin
				},
				spacing = dpi(5),
				layout = wibox.layout.fixed.vertical
			},
			margins = dpi(10),
			widget = wibox.container.margin
		},
		widget = background({ bg = "bgalt", fg = "fg" })
	}

	notif:get_children_by_id("remove")[1].buttons = { awful.button({}, 1, function() removenotif(notif) end) }

	return notif
end

local notifs = wibox.widget {
	{
		{
			{
				{
					{ id = "pageup", widget = notifbutton({ image = "up" }) },
					{ id = "pagedown", widget = notifbutton({ image = "down" }) },
					spacing = dpi(10),
					layout = wibox.layout.fixed.horizontal
				},
				nil,
				{ id = "clear", widget = notifbutton({ image = "close" }) },
				layout = wibox.layout.align.horizontal
			},
			{
				notifempty,
				notifcontainer,
				layout = wibox.layout.stack
			},
			spacing = dpi(10),
			layout = wibox.layout.fixed.vertical
		},
		margins = dpi(10),
		widget = wibox.container.margin
	},
	forced_width = dpi(220),
	forced_height = dpi(340),
	widget = background({ bg = "bgmid", fg = "fg" })
}

local notifposition = 1
local function notifmove(direction)
	if #notifcontainer.children > 0 then
		if direction == "down" then
			if notifposition < #notifcontainer.children then
				notifcontainer.children[notifposition].visible = false
				notifposition = notifposition + 1
			end
		elseif direction == "up" then
			if notifposition > 1 then
				notifposition = notifposition - 1
				notifcontainer.children[notifposition].visible = true
			end
		end
	end
end

notifs:get_children_by_id("pageup")[1].buttons = { awful.button({}, 1, function() notifmove("up") end) }
notifs:get_children_by_id("pagedown")[1].buttons = { awful.button({}, 1, function() notifmove("down") end) }
notifs:get_children_by_id("clear")[1].buttons = { awful.button({}, 1, function() notifcontainer:reset() notifempty.visible = true notifposition = 1 end) }

local activelen = 0
naughty.connect_signal("property::active", function()
	local notiflen = #naughty.active
	if notiflen > activelen then
		notifcontainer:insert(1, createnotif(naughty.active[notiflen]))
		notifempty.visible = false
	end
	activelen = notiflen
end)

return notifs


================================================
FILE: src/usr/share/calla/desktop/theme/control/profile.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi

local margins = dpi(0)
if beautiful.pfp == beautiful.calla then
	margins = dpi(10)
end

local profile = wibox.widget {
	{
		{
			{
				{
					{
						image = beautiful.pfp,
						upscale = false,
						downscale = true,
						valign = "center",
						halign = "center",
						clip_shape = function(cr, width, height)
							gears.shape.rounded_rect(cr, width, height, dpi(5))
						end,
						widget = live(wibox.widget.imagebox, { image = "pfp" })
					},
					margins = margins,
					widget = wibox.container.margin
				},
				forced_width = dpi(90),
				widget = background({ bg = "bgalt" })
			},
			{
				{
					{
						id = "name",
						text = "User Name",
						font = user.font:gsub("%d+", "14"),
						widget = wibox.widget.textbox
					},
					{
						id = "host",
						text = "@calla",
						widget = wibox.widget.textbox
					},
					spacing = dpi(5),
					layout = wibox.layout.fixed.vertical
				},
				valign = "center",
				widget = wibox.container.place
			},
			spacing = dpi(10),
			layout = wibox.layout.fixed.horizontal
		},
		margins = dpi(10),
		widget = wibox.container.margin
	},
	forced_width = dpi(220),
	forced_height = dpi(110),
	widget = background({ bg = "bgmid", fg = "fg" })
}

awful.spawn.easy_async_with_shell("getent passwd $(whoami) | cut -d ':' -f 5", function(out)
	profile:get_children_by_id("name")[1].text = out:gsub(",", ""):gsub("\n", "")
end)
awful.spawn.easy_async_with_shell("hostname", function(out)
	profile:get_children_by_id("host")[1].text = "@" .. out:gsub("\n", "")
end)

return profile


================================================
FILE: src/usr/share/calla/desktop/theme/control/sliders.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi

local function slider()
	return wibox.widget {
		bar_shape = gears.shape.rounded_rect,
		bar_height = dpi(5),
		bar_color = beautiful.bgalt,
		bar_active_color = beautiful.fg,
		handle_color = beautiful.fg,
		handle_shape = gears.shape.rounded_rect,
		handle_width = dpi(10),
		forced_height = dpi(10),
		value = 0,
		maximum = 100,
		widget = live(wibox.widget.slider, { bar_color = "bgalt", bar_active_color = "fg", handle_color = "fg" })
	}
end

local volumeslider = slider()

local volumepercent = wibox.widget.textbox()

local volumeicon = iconbox({ image = "volumemute" })

local volumehovering = false
local volumestore = 0
local mutestore = false

local function editvolume(volume, mute)
	if volume ~= nil then
		volumestore = volume
		mutestore = mute
	else
		volume = volumestore
		mute = mutestore
	end
	if not volumehovering then
		volumeslider.value = volume
	end
	if mute then
		volumepercent.text = "Muted"
		volumeicon.image = createicon("volumemute")
	else
		volumepercent.text = tostring(volume) .. "%"
		if volume > 100 then
			volumeicon.image = createicon("volumewarn")
		elseif volume >= 50 then
			volumeicon.image = createicon("volume100")
		elseif volume >= 25 then
			volumeicon.image = createicon("volume50")
		elseif volume > 0 then
			volumeicon.image = createicon("volume25")
		elseif volume == 0 then
			volumeicon.image = createicon("volume0")
		end
	end
end

awesome.connect_signal("signal::volume", function(volume, mute)
	editvolume(volume, mute)
end)
awesome.connect_signal("live::reload", function()
	editvolume()
end)

-- Very hacky workaround, vol will only be changed with signal when mouse is outside of slider
volumeslider:connect_signal("mouse::enter", function()
	volumehovering = true
end)
volumeslider:connect_signal("mouse::leave", function()
	volumehovering = false
end)
volumeslider:connect_signal("property::value", function(_, new)
	if volumehovering then
		awful.spawn.with_shell("wpctl set-mute @DEFAULT_AUDIO_SINK@ 0")
		awful.spawn.with_shell("wpctl set-volume @DEFAULT_AUDIO_SINK@ " .. new .. "%")
	end
end)

local volume = wibox.widget {
	{
		{
			{
				wibox.widget.textbox("Volume"),
				nil,
				volumepercent,
				layout = wibox.layout.align.horizontal
			},
			nil,
			{
				volumeicon,
				{
					volumeslider,
					forced_height = dpi(10),
					widget = wibox.container.place
				},
				spacing = dpi(10),
				layout = wibox.layout.fixed.horizontal
			},
			layout = wibox.layout.align.vertical
		},
		margins = dpi(10),
		widget = wibox.container.margin
	},
	forced_height = dpi(65),
	widget = background({ bg = "bgmid", fg = "fg" })
}

local brightnessslider = slider()

local brightnesspercent = wibox.widget.textbox()

local brightnessicon = iconbox({ image = "brightness0" })

local brightnesshovering = false
local brightnessstore = 0

local function editbrightness(brightness)
	if brightness ~= nil then
		brightnessstore = brightness
	else
		brightness = brightnessstore
	end
	if not brightnesshovering then
		brightnessslider.value = brightness
	end
	brightnesspercent.text = tostring(brightness) .. "%"
	if brightness >= 75 then
		brightnessicon.image = createicon("brightness100")
	elseif brightness >= 50 then
		brightnessicon.image = createicon("brightness75")
	elseif brightness >= 25 then
		brightnessicon.image = createicon("brightness50")
	elseif brightness > 0 then
		brightnessicon.image = createicon("brightness25")
	elseif brightness == 0 then
		brightnessicon.image = createicon("brightness0")
	end
end
awesome.connect_signal("signal::brightness", function(brightness)
	editbrightness(brightness)
end)
awesome.connect_signal("live::reload", function()
	editbrightness()
end)

brightnessslider:connect_signal("mouse::enter", function()
	brightnesshovering = true
end)
brightnessslider:connect_signal("mouse::leave", function()
	brightnesshovering = false
end)
brightnessslider:connect_signal("property::value", function(_, new)
	if brightnesshovering then
		awful.spawn.with_shell("brightnessctl s " .. new .. "%")
	end
end)

local brightness = wibox.widget {
	{
		{
			{
				wibox.widget.textbox("Brightness"),
				nil,
				brightnesspercent,
				layout = wibox.layout.align.horizontal
			},
			nil,
			{
				brightnessicon,
				{
					brightnessslider,
					forced_height = dpi(10),
					widget = wibox.container.place
				},
				spacing = dpi(10),
				layout = wibox.layout.fixed.horizontal
			},
			layout = wibox.layout.align.vertical
		},
		margins = dpi(10),
		widget = wibox.container.margin
	},
	forced_height = dpi(65),
	widget = background({ bg = "bgmid", fg = "fg" })
}

local sliders = wibox.widget {
	volume,
	brightness,
	spacing = dpi(10),
	widget = wibox.layout.fixed.vertical
}

return sliders


================================================
FILE: src/usr/share/calla/desktop/theme/control/system.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi

local function systembutton(widget)
	return button({ 
				image = widget.icon, 
				height = dpi(30), 
				width = dpi(32), 
				run = widget.run
			})
end

local system = wibox.widget {
	{
		{
			systembutton({ 
				icon = "settings", 
				run = function() 
					awesome.emit_signal("widget::control")
					awesome.emit_signal("widget::config") 
				end
			}),
			systembutton({ 
				icon = "shutdown", 
				run = function() 
					awful.spawn.with_shell(user.shutdown)
				end
			}),
			systembutton({ 
				icon = "restart", 
				run = function() 
					awful.spawn.with_shell(user.reboot)
				end
			}),
			systembutton({ 
				icon = "exit", 
				run = function() 
					awesome.quit()
				end
			}),
			systembutton({ 
				icon = "lock", 
				run = function() 
					awesome.emit_signal("widget::control")
					awesome.emit_signal("widget::lockscreen")
				end
			}),
			spacing = dpi(10),
			layout = wibox.layout.fixed.horizontal
		},
		margins = dpi(10),
		widget = wibox.container.margin
	},
	forced_width = dpi(220),
	forced_height = dpi(50),
	widget = background({ bg = "bgmid", fg = "fg" })
}

return system


================================================
FILE: src/usr/share/calla/desktop/theme/control/toggles.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local naughty = require("naughty")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi

local function togglebutton(widget)
	local switch = function(param, cases)
		local case = cases[param]
		if case then return case() end
	end

	local onicon, officon
	local icon = iconbox({ image = "error" })

	local state
	local iscompositor, compositorinit
	if widget == "compositor" then iscompositor, compositorinit = true, false end

	local button = hovercursor(wibox.widget {
		{
			icon,
			margins = dpi(5),
			widget = wibox.container.margin
		},
		forced_width = dpi(95),
		forced_height = dpi(50),
		widget = background({ bg = "bgalt", fg = "fg" })
	})

	local function init()
		switch(widget, {
			["wifi"] = function()
				onicon, officon = "wifion", "wifioff"
			end,
			["bluetooth"] = function()
				onicon, officon = "bluetoothon", "bluetoothoff"
			end,
			["compositor"] = function()
				onicon, officon = "compositoron", "compositoroff"
			end,
			["notifs"] = function()
				onicon, officon = "notificationson", "notificationsoff"
			end
		})
	end

	local function getstate()
		local function finish()
			if state then
				icon.image = createicon(onicon)
				button.bg = beautiful.fg .. 33
			else
				icon.image = createicon(officon)
				button.bg = beautiful.bgalt
			end
		end

		switch(widget, {
			["wifi"] = function()
				state = awful.spawn.easy_async_with_shell("nmcli radio wifi", function(out)
					if out:match("enabled") then
						 state = true
					else
						 state = false
					end
					finish()
				end)
			end,
			["bluetooth"] = function()
				state = awful.spawn.easy_async_with_shell("bluetoothctl show | grep 'Powered: yes'", function(out)
					if out ~= "" then
						 state = true
					else
						 state = false
					end
					finish()
				end)
			end,
			["compositor"] = function()
				awful.spawn.easy_async_with_shell("ps aux | grep picom | grep -v grep", function(out)
					if not compositorinit then awesome.emit_signal("compositor::init") compositorinit = true end
					if out ~= "" then
						 state = true
					else
						 state = false
					end
					finish()
				end)
			end,
			["notifs"] = function()
				state = not naughty.suspended
				finish()
			end
		})
	end

	local function changestate()
		switch(widget, {
			["wifi"] = function()
				local command
				if state then
					command = "off"
				else
					command = "on"
				end
				awful.spawn.easy_async_with_shell("nmcli radio wifi " .. command, function() getstate() end)
			end,
			["bluetooth"] = function()
				local command
				if state then
					command = "off"
				else
					command = "on"
				end
				awful.spawn.easy_async_with_shell("bluetoothctl power " .. command, function() getstate() end)
			end,
			["compositor"] = function()
				if state then
					awful.spawn.easy_async_with_shell("killall picom", function() getstate() end)
				else
					awful.spawn.easy_async_with_shell("picom -b --config '/usr/share/calla/compositor.conf'", function() getstate() end)
				end
			end,
			["notifs"] = function()
				naughty.suspended = not naughty.suspended
				getstate()
			end
		})
	end

	if iscompositor then
		awesome.connect_signal("compositor::init", function()
			init()
			getstate()
			button.buttons = { awful.button({}, 1, changestate) }
		end)
	else
		init()
		getstate()
		button.buttons = { awful.button({}, 1, changestate) }
	end
	awesome.connect_signal("live::reload", function()
		getstate()
	end)

	return button
end

local toggles = wibox.widget {
	{
		{
			togglebutton("wifi"),
			togglebutton("bluetooth"),
			togglebutton("compositor"),
			togglebutton("notifs"),
			column_count = 2,
			spacing = dpi(10),
			layout = wibox.layout.grid
		},
		margins = dpi(10),
		widget = wibox.container.margin
	},
	forced_width = dpi(220),
	forced_height = dpi(130),
	widget = background({ bg = "bgmid", fg = "fg" })
}

return toggles



================================================
FILE: src/usr/share/calla/desktop/theme/desktop.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi
local icons = "/usr/share/icons/" .. beautiful.icons .. "/64x64/"
local desktopjson = gears.filesystem.get_cache_dir() .. "desktop.json"
local lgi = require("lgi")
local Gtk = lgi.require("Gtk", "3.0")
local Gio = lgi.Gio
local UPower = lgi.require("UPowerGlib")

screen.connect_signal("request::desktop_decoration", function(s)

	--[[ logic for grabbing batteries, for use in future desktop widgets
	local function listdevices()
		local ret = {}
		local devices = UPower.Client():get_devices()

		for _, device in ipairs(devices) do
			table.insert(ret, device:get_object_path())
		end

		return ret
	end

	local function getdevice(path)
		local devices = UPower.Client():get_devices()

		for _, device in ipairs(devices) do
			if device:get_object_path() == path then
				return device
			end
		end

		return nil
	end

	local devicepaths = listdevices()

	for _, path in ipairs(devicepaths) do
		local device = getdevice(path)

		device.on_notify = function()
		end
	end
	--]]

	local cell = dpi(120)
	local geometry = s:get_bounding_geometry()
	local rows = math.floor((geometry.height-dpi(60))/cell)
	local cols = math.floor((geometry.width-dpi(20))/cell)
	local vspacing = math.floor((((geometry.height-dpi(60))-(rows*cell))/(rows-1))+0.5)
	local hspacing = math.floor((((geometry.width-dpi(20))-(cols*cell))/(cols-1))+0.5)

	s.grid = wibox.widget {
		forced_num_rows = rows,
		forced_num_cols = cols,
		vertical_spacing = vspacing,
		horizontal_spacing = hspacing,
		orientation = "horizontal",
		layout = wibox.layout.grid
	}

	s.manual = wibox.layout {
		layout = wibox.layout.manual
	}

	s.padding = {
		top = dpi(10),
		bottom = dpi(50),
		left = dpi(10),
		right = dpi(10)
	}

	s.base = wibox {
		screen = s,
		ontop = false,
		visible = true,
		type = "splash"
	}

	s.desktop = wibox {
		screen = s,
		width = s.geometry.width-dpi(20),
		height = s.geometry.height-dpi(60),
		ontop = false,
		visible = true,
		type = "desktop"
	}

	awful.placement.maximize(s.base)

	awful.placement.top(
		s.desktop,
		{
			margins = {
				top = dpi(10)
			}
		}
	)

	s.panel = require("theme.panel")(s)

	s.base:setup {
		{
			s.panel,
			valign = "bottom",
			content_fill_horizontal = true,
			widget = wibox.container.place
		},
		widget = live(wibox.widget.background, { bg = "bg" })
	}

	s.desktop:setup {
		{
			id = "wallpaper",
			image = gears.surface.crop_surface {
				surface = gears.surface.load_uncached(beautiful.wallpaper),
				ratio = (s.geometry.width-dpi(20))/(s.geometry.height-dpi(60))
			},
			widget = wibox.widget.imagebox
		},
		s.grid,
		s.manual,
		layout = wibox.layout.stack
	}

	local function generate()
		local entries = {}

		for entry in io.popen([[ls ~/Desktop | sed '']]):lines() do
			local label = nil
			local exec = nil
			local icon = nil

			if entry:match("^.+(%..+)$") == ".desktop" then
				for line in io.popen("cat ~/Desktop/'" .. entry .. "'"):lines() do
					if line:match("Name=") and not label then
						label = line:gsub("Name=", "")
					end
					if line:match("Exec=") and not exec then
						exec = line:gsub("Exec=", ""):gsub("%%U", ""):gsub("%%u", "")
					end
					if line:match("CustomIcon=") and not icon then
						icon = line:gsub("CustomIcon=", "")
					elseif line:match("Icon=") and not icon then
						icon = line:gsub("Icon=", "")
					end
				end
				table.insert(entries, { label = label, exec = exec, icon = icon })
			elseif os.execute("cd ~/Desktop'" .. entry .. "'") then
				label = entry
				icon = "folder"
				exec = "gio open ~/Desktop/'" .. entry .. "'"
				table.insert(entries, { label = label, exec = exec, icon = icon })
			else
				label = entry
				icon = Gio.File.new_for_path(os.getenv("HOME") .. "/Desktop/" .. entry):query_info("standard::*", Gio.FileQueryInfoFlags.NONE):get_icon()
				for _, name in ipairs(icon:get_names()) do
					if Gtk.IconTheme.get_default():has_icon(name) then
						icon = name
						break
					end
					icon = "application-x-generic"
				end
				exec = "gio open ~/Desktop/'" .. entry .. "'"
				table.insert(entries, { label = label, exec = exec, icon = icon })
			end
		end

		return entries
	end

	local function save()
		layout = {}

		for i, widget in ipairs(s.grid.children) do
			local pos = s.grid:get_widget_position(widget)

			layout[i] = {
				row = pos.row,
				col = pos.col,
				widget = {
					label = widget.label,
					exec = widget.exec,
					icon = widget.icon
				}
			}
		end

		writejson(desktopjson, layout)
	end

	local function gridindexat(y, x) -- gotta fix this to account for spacing
		local margin = dpi(10)

		local row = math.ceil((y - margin) / cell)
		row = math.min(row, rows)
		row = math.max(row, 1)

		local col = math.ceil((x - margin) / cell)
		col = math.min(col, cols)
		col = math.max(col, 1)

		return row, col
	end

	local function exists(path)
		local f = io.open(path, "r")
		if f~=nil then io.close(f) return true else return false end
	end

	local function createdesktopicon(label, exec, icon)
		local image
		if exists(icons .. "places/" .. icon .. ".svg") then
			image = icons .. "places/" .. icon .. ".svg"
		elseif exists(icons .. "mimetypes/" .. icon .. ".svg") then
			image = icons .. "mimetypes/" .. icon .. ".svg"
		elseif exists(icons .. "apps/" .. icon .. ".svg") then
			image = icons .. "apps/" .. icon .. ".svg"
		elseif exists(icon) then
			image = icon
		else
			image = icons .. "mimetypes/application-x-generic.svg"
		end

		local widget = hovercursor(wibox.widget {
			{
				{
					{
						image = image,
						halign = "center",
						widget = wibox.widget.imagebox
					},
					strategy = "exact",
					width = dpi(50),
					height = dpi(50),
					widget = wibox.container.constraint
				},
				{
					{
						{
							{
								valign = "top",
								align = "center",
								widget = colortext({ text = label })
							},
							margins = dpi(5),
							widget = wibox.container.margin
						},
						widget = background({ bg = "bgmid" })
					},
					strategy = "max",
					width = dpi(100),
					height = dpi(50),
					widget = wibox.container.constraint
				},
				spacing = dpi(5),
				layout = wibox.layout.fixed.vertical
			},
			label = label,
			exec = exec,
			icon = icon,
			forced_width = cell,
			forced_height = cell,
			top = dpi(10),
			left = dpi(10),
			right = dpi(10),
			widget = wibox.container.margin
		})

		widget:connect_signal("button::press", function(_, _, _, button)
			if not mousegrabber.isrunning() then
				local heldwidget = wibox.widget {
					{
						{
							{
								image = image,
								opacity = 0.5,
								halign = "center",
								widget = wibox.widget.imagebox
							},
							strategy = "exact",
							width = dpi(50),
							height = dpi(50),
							widget = wibox.container.constraint
						},
						{
							{
								{
									{
										opacity = 0.5,
										valign = "top",
										align = "center",
										widget = colortext({ text = label })
									},
									margins = dpi(5),
									widget = wibox.container.margin
								},
								widget = background({ bg = "bgmid" })
							},
							strategy = "max",
							width = dpi(100),
							height = dpi(50),
							widget = wibox.container.constraint
						},
						spacing = dpi(5),
						layout = wibox.layout.fixed.vertical
					},
					forced_width = cell,
					forced_height = cell,
					top = dpi(10),
					left = dpi(10),
					right = dpi(10),
					visible = false,
					widget = wibox.container.margin
				}

				local startpos = mouse.coords()
				heldwidget.point = { x = startpos.x, y = startpos.y }
				local oldpos = s.grid:get_widget_position(widget)
				s.manual:add(heldwidget)

				mousegrabber.run(function(mouse)
					if (math.abs(mouse.x - startpos.x) > 10 or
						math.abs(mouse.y - startpos.y) > 10) and
						mouse.buttons[1] then

						s.grid:remove(widget)
						heldwidget.visible = true

						s.manual:move_widget(heldwidget, {
							x = mouse.x - dpi(50),
							y = mouse.y - dpi(50)
						})
					end

					if not mouse.buttons[1] then
						if button == 1 then
							if heldwidget.visible then
								heldwidget.visible = false

								local newrow, newcol = gridindexat(
									mouse.y,
									mouse.x
								)
								if not s.grid:get_widgets_at(newrow, newcol) then
									s.grid:add_widget_at(widget, newrow, newcol)
									save()
								else
									s.grid:add_widget_at(widget, oldpos.row, oldpos.col)
								end
							else
								awful.spawn.with_shell(exec)
								s.manual:reset()
							end
							mousegrabber.stop()
						end
					end
					return mouse.buttons[1]
				end, "hand2")
			end
		end)

		return widget
	end

	local function load()
		s.grid:reset()

		if not gears.filesystem.file_readable(desktopjson) then
			local entries = generate()
			for _, entry in ipairs(entries) do
				s.grid:add(createdesktopicon(entry.label, entry.exec, entry.icon))
			end
			save()
			return
		end

		local layout = readjson(desktopjson)

		for _, entry in ipairs(layout) do
			s.grid:add_widget_at(createdesktopicon(entry.widget.label, entry.widget.exec, entry.widget.icon), entry.row, entry.col)
		end
	end

	load()

	awesome.connect_signal("signal::desktop", function(type)
		local entries = generate()
		local check = false

		if type == "add" then
			for _, entry in ipairs(entries) do
				for _, widget in ipairs(s.grid.children) do
					if entry.label == widget.label then
						check = true
						break
					end
				end
				if check == false then
					s.grid:add(createdesktopicon(entry.label, entry.exec, entry.icon))
				end
				check = false
			end
		end

		if type == "remove" then
			for _, widget in ipairs(s.grid.children) do
				for _, entry in ipairs(entries) do
					if entry.label == widget.label then
						check = true
						break
					end
				end
				if check == false then
					s.grid:remove(widget)
				end
				check = false
			end
		end

		save()
	end)

	awesome.connect_signal("live::reload", function()
		s.desktop:get_children_by_id("wallpaper")[1].image = gears.surface.crop_surface {
			surface = gears.surface.load_uncached(beautiful.wallpaper),
			ratio = (s.geometry.width-dpi(20))/(s.geometry.height-dpi(60))
		}
	end)

end)


================================================
FILE: src/usr/share/calla/desktop/theme/dock.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi
local lgi = require("lgi")
local Gtk = lgi.require("Gtk", "3.0")
local dockjson = gears.filesystem.get_cache_dir() .. "dock.json"

local tasklist

local pins = wibox.widget {
	spacing = dpi(5),
	layout = wibox.layout.fixed.horizontal
}

local separator = wibox.widget {
	orientation = "vertical",
	thickness = dpi(2),
	span_ratio = 0.75,
	forced_width = dpi(5),
	visible = false,
	widget = live(wibox.widget.separator, { color = "bgalt" })
}

if not gears.filesystem.file_readable(dockjson) then
	writejson(dockjson, {})
end
local pinned = readjson(dockjson)

local function pin(class, exec)
	local theme = Gtk.IconTheme.get_default()
	local icon = theme:lookup_icon(class:lower(), 64, 0)
	if icon then
		icon = icon:get_filename()
	else
		icon = require("menubar").utils.lookup_icon_uncached(class:lower())
		if not icon then
			icon = theme:lookup_icon("application-default-icon", 64, 0):get_filename()
		end
	end
	local widget = hovercursor(wibox.widget {
		{
			{
				{
					shape = function(cr, width, height)
								gears.shape.rounded_rect(cr, width, height, dpi(8))
							end,
					id = "background",
					bg = beautiful.bg,
					widget = wibox.container.background
				},
				margins = dpi(2),
				widget = wibox.container.margin
			},
			shape = function(cr, width, height)
						gears.shape.rounded_rect(cr, width, height, dpi(10))
					end,
			id = "foreground",
			bg = beautiful.fg,
			widget = wibox.container.background
		},
		{
			wibox.widget.imagebox(icon),
			margins = dpi(5),
			widget = wibox.container.margin
		},
		layout = wibox.layout.stack
	})

	local function check()
		local present = false
		local focused = false
		if client.focus and client.focus.class == class then
			widget:get_children_by_id("background")[1].bg = beautiful.bgalt
			widget:get_children_by_id("foreground")[1].bg = beautiful.fg .. "64"
			widget.buttons = {
				awful.button({}, 1, function()
					for _, c in ipairs(client.get()) do
						if c.class == class then
							c.minimized = false
							c:raise()
						end
					end
				end),
				awful.button({ "Shift" }, 1, function()
					awful.spawn.with_shell(exec)
				end),
				awful.button({}, 3, function()
					for i, app in ipairs(pinned) do
						if app.class == class then
							table.remove(pinned, i)
						end
					end
					pins:remove(pins:index(widget))
					tasklist._do_tasklist_update_now()
					writejson(dockjson, pinned)
				end)
			}
			present = true
			focused = true
		end
		if not focused then
			for _, c in ipairs(client.get()) do
				if c.class == class then
					widget:get_children_by_id("background")[1].bg = beautiful.bgalt
					widget:get_children_by_id("foreground")[1].bg = beautiful.bgalt
					widget.buttons = {
						awful.button({}, 1, function()
							c.first_tag:view_only() -- check current tag first?
							for _, c in ipairs(client.get()) do
								if c.class == class then
									c.minimized = false
									c:raise()
									c:activate()
								end
							end
						end),
						awful.button({ "Shift" }, 1, function()
							awful.spawn.with_shell(exec)
						end),
						awful.button({}, 3, function()
							for i, app in ipairs(pinned) do
								if app.class == class then
									table.remove(pinned, i)
								end
							end
							pins:remove(pins:index(widget))
							tasklist._do_tasklist_update_now()
							writejson(dockjson, pinned)
						end)
					}
					present = true
					return
				end
			end
		end
		if not present then
			widget:get_children_by_id("background")[1].bg = beautiful.bg
			widget:get_children_by_id("foreground")[1].bg = beautiful.bg
			widget.buttons = {
				awful.button({}, 1, function()
					awful.spawn.with_shell(exec)
				end),
				awful.button({}, 3, function()
					for i, app in ipairs(pinned) do
						if app.class == class then
							table.remove(pinned, i)
						end
					end
					pins:remove(pins:index(widget))
					tasklist._do_tasklist_update_now()
					writejson(dockjson, pinned)
				end)
			}
		end
		widget:emit_signal("widget::redraw_needed")
	end

	client.connect_signal("request::manage", check)
	client.connect_signal("request::unmanage", check)
	client.connect_signal("focus", check)
	client.connect_signal("unfocus", check)
	awesome.connect_signal("live::reload", check)

	check()

	return widget
end

local function contains(table, name)
	for _, app in ipairs(table) do
		if app == name then
			return true
		end
	end
	return false
end

tasklist = awful.widget.tasklist {
	screen = awful.screen.focused(),
	filter = awful.widget.tasklist.filter.allscreen,
	source = function()
		local seen = {}
		local ret = {}

		for _, c in ipairs(client.get()) do
			local exclude = false
			for _, app in ipairs(pinned) do
				if c.class == app.class then
					exclude = true
					break
				end
			end
			if not exclude and not contains(seen, c.class) or c.minimized == true then
				table.insert(seen, c.class)
				table.insert(ret, c)
			end
		end

		if seen[1] and pinned[1] then
			separator.visible = true
		else
			separator.visible = false
		end

		return ret
	end,
	style = {
		shape = function(cr, width, height)
					gears.shape.rounded_rect(cr, width, height, dpi(10))
				end
	},
	layout = {
		spacing = dpi(5),
		spacing_widget = wibox.container.background,
		layout = wibox.layout.fixed.horizontal
	},
	widget_template = {
		{
			{
				{
					awful.widget.clienticon,
					margins = dpi(5),
					widget = wibox.container.margin
				},
				shape = function(cr, width, height)
							gears.shape.rounded_rect(cr, width, height, dpi(8))
						end,
				id = "background",
				widget = wibox.widget.background
			},
			margins = dpi(2),
			widget = wibox.container.margin
		},
		shape = function(cr, width, height)
					gears.shape.rounded_rect(cr, width, height, dpi(10))
				end,
		id = "foreground",
		bg = beautiful.fg,
		widget = wibox.container.background,
		create_callback = function(self, c)
			local exec
			if c.pid then
				awful.spawn.easy_async("readlink -f /proc/" .. c.pid .. "/exe", function(out)
					exec = out:gsub("\n", "")
				end)
			end
			self.buttons = {
				awful.button({}, 1, function()
					c.first_tag:view_only()
					c.minimized = false
					c:raise()
				end),
				awful.button({}, 3, function()
					local seen
					for _, app in ipairs(pinned) do
						if app.class == c.class then
							seen = true
							return
						end
					end
					if not seen then
						pins:add(pin(c.class, exec))
						table.insert(pinned, { class = c.class, exec = exec })
						tasklist._do_tasklist_update_now()
						writejson(dockjson, pinned)
					end
				end)
			}
			hovercursor(self)

			if client.focus == c then
				self:get_children_by_id("background")[1].bg = beautiful.bgalt
				self:get_children_by_id("foreground")[1].bg = beautiful.fg .. "64"
			else
				self:get_children_by_id("background")[1].bg = beautiful.bgalt
				self:get_children_by_id("foreground")[1].bg = beautiful.bgalt
			end
			client.connect_signal("focus", function()
				if client.focus == c then
					self:get_children_by_id("background")[1].bg = beautiful.bgalt
					self:get_children_by_id("foreground")[1].bg = beautiful.fg .. "64"
				else
					self:get_children_by_id("background")[1].bg = beautiful.bgalt
					self:get_children_by_id("foreground")[1].bg = beautiful.bgalt
				end
			end)
			client.connect_signal("unfocus", function()
				self:get_children_by_id("background")[1].bg = beautiful.bgalt
				self:get_children_by_id("foreground")[1].bg = beautiful.bgalt
			end)
			awesome.connect_signal("live::reload", function()
				if client.focus == c then
					self:get_children_by_id("background")[1].bg = beautiful.bgalt
					self:get_children_by_id("foreground")[1].bg = beautiful.fg .. "64"
				else
					self:get_children_by_id("background")[1].bg = beautiful.bgalt
					self:get_children_by_id("foreground")[1].bg = beautiful.bgalt
				end
			end)
		end
	}
}

for _, app in ipairs(pinned) do
	pins:add(pin(app.class, app.exec))
end

local dock = wibox.widget {
	{
		pins,
		separator,
		tasklist,
		spacing = dpi(5),
		layout = wibox.layout.fixed.horizontal
	},
	halign = "center",
	widget = wibox.container.place
}

return dock


================================================
FILE: src/usr/share/calla/desktop/theme/init.lua
================================================
local beautiful = require("beautiful")
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local dpi = beautiful.xresources.apply_dpi

beautiful.init(gears.filesystem.get_configuration_dir() .. "theme/theme.lua")

screen.connect_signal("request::desktop_decoration", function(s)
	awesome.connect_signal("live::reload", function()
		awful.wallpaper {
			screen = s,
			bg = beautiful.bg
		}
	end)
end)

function live(w, properties)
    local widget = w()

	for property, arg in pairs(properties) do
		widget[property] = beautiful[arg]
	end

    awesome.connect_signal("live::reload", function()
		for property, arg in pairs(properties) do
			widget[property] = beautiful[arg]
		end
        widget:emit_signal("widget::redraw_needed")
    end)

    return widget
end

function background(properties)
	return wibox.widget {
		shape = function(cr, width, height)
					gears.shape.rounded_rect(cr, width, height, dpi(10))
				end,
		widget = live(wibox.container.background, properties)
	}
end

function hovercursor(widget)
	--local oldcursor, oldwibox, oldbg
	local oldcursor, oldwibox
	widget:connect_signal("mouse::enter", function()
		local wb = mouse.current_wibox
		if wb == nil then return end
		--oldcursor, oldwibox, oldbg = wb.cursor, wb, wb.bg
		oldcursor, oldwibox = wb.cursor, wb
		wb.cursor = "hand2"
		--widget.bg = beautiful.fg .. "20"
	end)
	widget:connect_signal("mouse::leave", function()
		if oldwibox then
			oldwibox.cursor = oldcursor
			--widget.bg = oldbg
			oldwibox = nil
		end
	end)
	return widget
end

function markup(args)
	local fg = beautiful[args.fg] or beautiful.fg
	local text = '<span foreground="' .. fg .. '">' .. args.text .. '</span>'
	return text
end

function colortext(args)
	local table = args or {}
	local fg = beautiful[table.fg] or beautiful.fg
	local font = table.font or user.font
	local text = table.text or "N/A"
	local textbox = wibox.widget {
		markup = markup({ text = text, fg = fg }),
		font = font,
		widget = wibox.widget.textbox
	}

	awesome.connect_signal("live::reload", function()
		textbox.markup = markup({ text = text, fg = fg })
	end)

	return textbox
end

function createicon(image)
	local icon = gears.color.recolor_image(gears.filesystem.get_configuration_dir() .. "theme/icons/" .. image .. ".svg", beautiful.fg)

	awesome.connect_signal("live::reload", function()
		icon = gears.color.recolor_image(gears.filesystem.get_configuration_dir() .. "theme/icons/" .. image .. ".svg", beautiful.fg)
	end)

	return icon
end

function iconbox(widget)
	local size = dpi(18)
	if widget.size then size = widget.size end
	local icon = wibox.widget {
		image = gears.color.recolor_image(gears.filesystem.get_configuration_dir() .. "theme/icons/" .. widget.image .. ".svg", beautiful.fg),
		forced_width = size,
		forced_height = size,
		upscale = false,
		downscale = true,
		valign = "center",
		halign = "center",
		widget = wibox.widget.imagebox
	}

	awesome.connect_signal("live::reload", function()
		icon.image = gears.color.recolor_image(gears.filesystem.get_configuration_dir() .. "theme/icons/" .. widget.image .. ".svg", beautiful.fg)
	end)

	return icon
end

function button(widget)
	local img = iconbox({ image = widget.image })

	if widget.size then
		width = widget.size
		height = widget.size
	else
		if widget.height then
			height = widget.height
		else
			height = dpi(30)
		end

		if widget.width then
			width = widget.width
		else
			width = dpi(30)
		end
	end

	local button = hovercursor(wibox.widget {
		{
			img,
			margins = dpi(5),
			widget = wibox.container.margin
		},
		forced_width = width,
		forced_height = height,
		buttons = { awful.button({}, 1, widget.run) },
		widget = background({ bg = "bgmid" })
	})

	return button
end

require("theme.desktop")
require("theme.notif")
require("theme.title")
require("theme.volume")
require("theme.brightness")
require("theme.launcher")
require("theme.lock")
require("theme.settings")
require("theme.preview")
require("theme.control")


================================================
FILE: src/usr/share/calla/desktop/theme/launcher.lua
================================================
local wibox = require("wibox")
local awful = require("awful")
local gears = require("gears")
local Gio = require("lgi").Gio
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi
local appicons = "/usr/share/icons/" .. beautiful.icons .. "/64x64/apps/"

-- Widgets

local launcherbox = wibox {
	width = dpi(280),
	height = dpi(340),
	ontop = true,
	visible = false
}

local prompt = colortext({ text = "Search..." })

local textbox = wibox.widget {
	forced_height = dpi(30),
	widget = wibox.widget.textbox
}

local entries = wibox.widget {
	homogeneous = false,
	expand = true,
	forced_num_cols = 1,
	layout = wibox.layout.grid
}

launcherbox:setup {
	{
		{
			entries,
			nil,
			{
				{
					prompt,
					top = dpi(5),
					bottom = dpi(5),
					left = dpi(8),
					right = dpi(8),
					widget = wibox.container.margin
				},
				forced_height = dpi(30),
				widget = background({ bg = "bgmid" })
			},
			forced_width = dpi(300),
			layout = wibox.layout.align.vertical
		},
		margins = dpi(10),
		widget = wibox.container.margin
	},
	widget = live(wibox.container.background, { bg = "bg" })
}

-- Functions

local function next()
	if entryindex ~= #filtered then
		entries:get_widgets_at(entryindex, 1)[1]:get_children_by_id("bg")[1].bg = nil
		entries:get_widgets_at(entryindex+1, 1)[1]:get_children_by_id("bg")[1].bg = beautiful.bgmid
		entryindex = entryindex + 1
		if entryindex > startindex + 7 then
			entries:get_widgets_at(entryindex-8, 1)[1].visible = false
			entries:get_widgets_at(entryindex, 1)[1].visible = true
			startindex = startindex + 1
		end
	end
	move = true
end

local function back()
	if entryindex ~= 1 then
		entries:get_widgets_at(entryindex, 1)[1]:get_children_by_id("bg")[1].bg = nil
		entries:get_widgets_at(entryindex-1, 1)[1]:get_children_by_id("bg")[1].bg = beautiful.bgmid
		entryindex = entryindex - 1
		if entryindex < startindex then
			entries:get_widgets_at(entryindex+8, 1)[1].visible = false
			entries:get_widgets_at(entryindex, 1)[1].visible = true
			startindex = startindex - 1
		end
	end
	move = true
end

entries.buttons = {
	awful.button({}, 4, function()
		back()
	end),
	awful.button({}, 5, function()
		next()
	end)
}

local function gen()
	local entries = {}
	for _, entry in ipairs(Gio.AppInfo.get_all()) do
		if entry:should_show() then
			local name = entry:get_name():gsub("&", "&amp;"):gsub("<", "&lt;"):gsub("'", "&#39;")
			local icon = entry:get_icon()
			if icon then
				local name = icon:to_string()
				icon = appicons .. name .. ".svg"
				local function exists(file)
					local file=io.open(file, "r")
					if file ~= nil then 
						io.close(file) 
						return true 
					else 
						return false 
					end
				end
				if exists(icon) then
					icon = appicons .. name .. ".svg"
				else
					icon = appicons .. "application-default-icon.svg"
				end
			else
				icon = appicons .. "application-default-icon.svg"
			end
			table.insert(
				entries,
				{ icon = icon, name = name, appinfo = entry }
			)
		end
	end
	return entries
end

local function filter(cmd)

	filtered = {}
	regfiltered = {}
	
	-- Filter entries

	for _, entry in ipairs(unfiltered) do
		if entry.name:lower():sub(1, cmd:len()) == cmd:lower() then
			table.insert(filtered, entry)
		elseif entry.name:lower():match(cmd:lower()) then
			table.insert(regfiltered, entry)
		end
	end

	-- Sort entries

	table.sort(filtered, function(a, b) return a.name:lower() < b.name:lower() end)
	table.sort(regfiltered, function(a, b) return a.name:lower() < b.name:lower() end)

	-- Merge entries

	for i = 1, #regfiltered do
		filtered[#filtered+1] = regfiltered[i]
	end
	
	-- Clear entries

	entries:reset()

	-- Fix position

	entryindex, startindex = 1, 1

	-- Add filtered entries

	for i, entry in ipairs(filtered) do
		local widget = hovercursor(wibox.widget {
			{
				{
					{
						wibox.widget.imagebox(entry.icon),
						colortext({ text = entry.name }),
						spacing = dpi(5),
						layout = wibox.layout.fixed.horizontal
					},
					forced_height = dpi(30),
					margins = dpi(5),
					widget = wibox.container.margin
				},
				buttons = {
					awful.button({}, 1, function()
						if entryindex == i then
							local entry = filtered[entryindex]
							entry.appinfo:launch()
							awful.keygrabber.stop()
							launcherbox.visible = false
						else
							entries:get_widgets_at(entryindex, 1)[1]:get_children_by_id("bg")[1].bg = nil
							entryindex = i
							entries:get_widgets_at(entryindex, 1)[1]:get_children_by_id("bg")[1].bg = beautiful.bgmid
						end
					end)
				},
				id = "bg",
				shape = function(cr, width, height)
							gears.shape.rounded_rect(cr, width, height, dpi(10))
						end,
				widget = wibox.container.background
			},
			bottom = dpi(5),
			widget = wibox.container.margin
		})

		if startindex <= i and i <= startindex + 7 then
			widget.visible = true
		else
			widget.visible = false
		end

		entries:add(widget)

		if i == entryindex then
			widget:get_children_by_id("bg")[1].bg = beautiful.bgmid
		end
	end

	collectgarbage("collect")

end

local function open()

	-- Reset variables

	startindex, entryindex, move = 1, 1, false

	-- Get entries

	unfiltered = gen() -- TODO: Make this local
	filter("")

	-- Prompt

	awful.prompt.run {
		textbox = textbox,
		done_callback = function() 
			launcherbox.visible = false 
		end,
		changed_callback = function(cmd)
			if cmd ~= "" then
				prompt.markup = markup({ text = cmd, fg = "fg" })
			else
				prompt.markup = markup({ text = "Search...", fg = "fg" })
			end
			if move == false then	
				filter(cmd)
			else
				move = false
			end
		end,
		exe_callback = function(cmd)
			prompt.markup = markup({ text = "Search...", fg = "fg" })
			local entry = filtered[entryindex]
			if entry then
				entry.appinfo:launch()
			else
				awful.spawn.with_shell(cmd)
			end
		end,
		keypressed_callback = function(_, key)
			if key == "Down" then
				next()
			elseif key == "Up" then
				back()
			end
		end
	}
end

awesome.connect_signal("widget::launcher", function()
	awesome.emit_signal("widget::preview:hide")

	prompt.markup = markup({ text = "Search...", fg = "fg" })
	launcherbox.visible = not launcherbox.visible
	
	if launcherbox.visible then
		open()
	else
		awful.keygrabber.stop()
	end

	if client.focus and client.focus.fullscreen == true then
		awful.placement.bottom_left(
			launcherbox,
			{
				margins = {
					bottom = dpi(10),
					left = dpi(10),
				},
				parent = awful.screen.focused()
			}
		)
	else
		awful.placement.bottom_left(
			launcherbox,
			{
				margins = {
					bottom = dpi(60),
					left = dpi(20),
				},
				parent = awful.screen.focused()
			}
		)
	end
end)

awesome.connect_signal("widget::launcher:hide", function()
	launcherbox.visible = false
	awful.keygrabber.stop()
end)


================================================
FILE: src/usr/share/calla/desktop/theme/lock.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi

-- Authentication

local authenticate
local pamexists,pam = pcall(require,"liblua_pam")
if pamexists then
	authenticate = function(password)
		return pam.auth_current_user(password)
	end
else
	authenticate = function(password)
		return password == user.passwd
	end
end

-- Variables

local characters = 0

-- Background

screen.connect_signal("request::desktop_decoration", function(s)
	local lockbackground = wibox {
		screen = s,
		visible = false,
		ontop = true,
		type = "splash",
		widget = background({ bg = "bg" })
	}

	awful.placement.maximize(lockbackground)

	local batterypercent = wibox.widget {
		text = "N/A",
		widget = wibox.widget.textbox
	}

	local batteryicon = iconbox({ image = "batterynone" })

	local battery = wibox.widget {
		{
			{
				batteryicon,
				batterypercent,
				spacing = dpi(4),
				layout = wibox.layout.fixed.horizontal
			},
			left = dpi(8),
			right = dpi(8),
			widget = wibox.container.margin
		},
		widget = background({ bg = "bgmid", fg = "fg" })
	}

	local batterystore
	if user.batt ~= nil then
		awful.widget.watch("cat /sys/class/power_supply/" .. user.batt .. "/capacity", 15, function(widget, stdout)
			percent = tonumber(stdout)
			batterypercent.text = percent .. "%"
			if percent > 80 then
				batterystore = "battery100"
			elseif percent > 50 then
				batterystore = "battery80"
			elseif percent > 25 then
				batterystore = "battery50"
			elseif percent > 10 then
				batterystore = "battery25"
			elseif percent > 5 then
				batterystore = "battery10"
			else
				batterystore = "battery0"
			end
			batteryicon.image = createicon(batterystore)
		end)
	end

	if s == awful.screen.focused() then
		lockbackground:setup {
			{
				{
					{
						{
							{
								{
									wibox.widget.textbox("Lock Screen"),
									top = dpi(5),
									bottom = dpi(5),
									left = dpi(8),
									right = dpi(8),
									widget = wibox.container.margin
								},
								widget = background({ bg = "bgmid", fg = "fg" })
							},
							nil,
							{
								battery,
								{
									{
										{
											iconbox({ image = "clock" }),
											wibox.widget.textclock('%I:%M %p'),
											spacing = dpi(4),
											layout = wibox.layout.fixed.horizontal
										},
										left = dpi(8),
										right = dpi(8),
										widget = wibox.container.margin
									},
									widget = background({ bg = "bgmid", fg = "fg" })
								},
								spacing = dpi(5),
								layout = wibox.layout.fixed.horizontal
							},
							layout = wibox.layout.align.horizontal
						},
						{
							{
								button {
									image = "shutdown", 
									run = function() 
										awful.spawn.with_shell(user.shutdown)
									end
								},
								button {
									image = "restart", 
									run = function() 
										awful.spawn.with_shell(user.reboot)
									end
								},
								button {
									image = "exit", 
									run = function() 
										awesome.quit()
									end
								},
								spacing = dpi(5),
								layout = wibox.layout.fixed.horizontal
							},
							halign = "center",
							widget = wibox.container.place
						},
						layout = wibox.layout.stack
					},
					forced_height = dpi(50),
					margins = dpi(10),
					widget = wibox.container.margin
				},
				valign = "bottom",
				content_fill_horizontal = true,
				widget = wibox.container.place
			},
			widget = background({ bg = "bg" })
		}
	end

	local wallpaper = wibox {
		screen = awful.screen.focused(),
		width = s.geometry.width-dpi(20),
		height = s.geometry.height-dpi(60),
		ontop = true,
		visible = false,
		type = "desktop",
		widget = wibox.widget {
			image = gears.surface.crop_surface {
				surface = gears.surface.load_uncached(beautiful.wallpaper),
				ratio = (s.geometry.width-dpi(20))/(s.geometry.height-dpi(60))
			},
			widget = wibox.widget.imagebox
		}
	}

	awful.placement.top(
		wallpaper,
		{
			margins = {
				top = dpi(10)
			}
		}
	)

	awesome.connect_signal("live::reload", function()
		if batterystore then batteryicon.image = createicon(batterystore) end
		wallpaper.widget = wibox.widget {
			image = gears.surface.crop_surface {
				surface = gears.surface.load_uncached(beautiful.wallpaper),
				ratio = (s.geometry.width-dpi(20))/(s.geometry.height-dpi(60))
			},
			widget = wibox.widget.imagebox
		}
	end)

	awesome.connect_signal("lockscreen::visible", function(v)
		lockbackground.visible = v
		wallpaper.visible = v
	end)
end)

-- Prompt

local prompt = wibox.widget {
	font = user.font,
	align = "center",
	widget = colortext({ text = "Enter Password", fg = "fg" })
}

local promptbox = wibox {
	width = dpi(350),
	height = dpi(135),
	ontop = true,
	visible = false
}

awful.spawn.easy_async_with_shell("getent passwd $(whoami) | cut -d ':' -f 5", function(out)
	promptbox:setup {
		{
			{
				{
					{
						{
							live(wibox.widget.imagebox, { image = "calla" }),
							forced_height = dpi(24),
							margins = dpi(5),
							widget = wibox.container.margin
						},
						widget = background({ bg = "bgmid" })
					},
					{
						font = user.font:gsub("%d+", "24"),
						widget = colortext({ text = out:gsub(",", ""):gsub("\n", "") })
					},
					spacing = dpi(10),
					layout = wibox.layout.fixed.horizontal
				},
				{
					{
						{
							prompt,
							layout = wibox.layout.fixed.horizontal
						},
						top = dpi(10),
						bottom = dpi(10),
						left = dpi(15),
						right = dpi(15),
						forced_height = dpi(40),
						forced_width = dpi(300),
						widget = wibox.container.margin
					},
					widget = background({ bg = "bgmid" })
				},
				spacing = dpi(10),
				layout = wibox.layout.fixed.vertical
			},
			valign = "center",
			halign = "center",
			layout = wibox.container.place
		},
		widget = background({ bg = "bg" })
	}
end)

awful.placement.centered(promptbox)

awesome.connect_signal("lockscreen::visible", function(v)
	promptbox.visible = v
end)

-- Reset

local function reset()
	characters = 0
	prompt.markup = markup({ text = "Enter Password", fg = "fg" })
	--awful.spawn.with_shell("pkill --full --uid " .. os.getenv("USER") .. " fprintd-verify")
end

-- Fail

local function fail()
	characters = 0
	prompt.markup = markup({ text = "Try Again", fg = "fg" })
end

-- Input

local function grabpassword()
	awful.prompt.run {
		hooks = {
			{{ }, 'Escape', function(_)
					reset()
					grabpassword()
				end
			}
		},
		keypressed_callback  = function(mod, key, cmd)
			if #key == 1 then
				characters = characters + 1
				prompt.markup = markup({ text = string.rep("", characters), fg = "fg" })
			elseif key == "BackSpace" then
				if characters > 1 then
					characters = characters - 1
					prompt.markup = markup({ text = string.rep("", characters), fg = "fg" })
				else
					characters = 0
					prompt.markup = markup({ text = "Enter Password", fg = "fg" })
				end
			end
		end,
		exe_callback = function(input)
			--[[ Why doesn't this work?
			characters = 0
			prompt.markup = markup({ text = "Authenticating...", fg = "fg" })
			--]]
			if authenticate(input) then
				reset()
				awesome.emit_signal("lockscreen::visible", false)
			else
				fail()
				grabpassword()
			end
		end,
		textbox = wibox.widget.textbox()
	}

	--[[awful.spawn.easy_async("fprintd-verify", function(out)
		if out:match("verify%-match") then
			awful.keygrabber.stop()
			reset()
			awesome.emit_signal("lockscreen::visible", false)
		elseif out:match("verify%-no%-match") then
			awful.keygrabber.stop()
			fail()
			grabpassword()
		elseif out:match("already claimed") then
			awful.keygrabber.stop()
			reset()
			grabpassword()
		elseif out:match("verify%-unknown%-error") or out:match("already been opened") then
			require("naughty").notification{text="It appears that fprintd has encountered an error because of a suspend. Please enter password manually.",urgency="critical"}
		end
	end)--]]
end

-- Lock

awesome.connect_signal("widget::lockscreen", function()
	awesome.emit_signal("lockscreen::visible", true)
	grabpassword()
end)


================================================
FILE: src/usr/share/calla/desktop/theme/notif.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local ruled = require("ruled")
local naughty = require("naughty")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi

-- Config

naughty.config.defaults.ontop = true
naughty.config.defaults.screen = awful.screen.focused()
naughty.config.defaults.border_width = 0
naughty.config.defaults.position = "top_right"
naughty.config.defaults.title = "Notification"

-- Rules

ruled.notification.connect_signal('request::rules', function()
	local function setrules()
		-- Critical
		ruled.notification.append_rule {
			rule       = { urgency = 'critical' },
			properties = { bg = beautiful.bg_normal, fg = beautiful.fg_urgent, timeout = 0 }
		}

		-- Normal
		ruled.notification.append_rule {
			rule       = { urgency = 'normal' },
			properties = { bg = beautiful.bg_normal, fg = beautiful.fg_normal, timeout = 5 }
		}

		-- Low
		ruled.notification.append_rule {
			rule       = { urgency = 'low' },
			properties = { bg = beautiful.bg_normal, fg = beautiful.fg_normal, timeout = 5 }
		}
	end

	setrules()

	awesome.connect_signal("live::reload", function()
		setrules()
	end)
end)

-- Notification

naughty.connect_signal("request::display", function(n)
	naughty.layout.box {
		notification = n,
		type = "notification",
		bg = beautiful.bg_normal,
		widget_template = {
			{
				{
					{
						{
							{
								{
									{
										naughty.widget.title,
										layout = wibox.layout.align.horizontal
									},
									margins = dpi(10),
									widget = wibox.container.margin
								},
								shape = function(cr, width, height)
											gears.shape.rounded_rect(cr, width, height, dpi(10))
										end,
								widget = live(wibox.container.background, { bg = "bgmid" })
							},
							strategy = "min",
							width = dpi(250),
							widget = wibox.container.constraint
						},
						{
								naughty.widget.message,
								margins = dpi(5),
								widget = wibox.container.margin
						},
						spacing = dpi(5),
						layout = wibox.layout.fixed.vertical
					},
					strategy = "max",
					width = dpi(400),
					widget = wibox.container.constraint
				},
				margins = dpi(10),
				widget = wibox.container.margin
			},	
			id = "background_role",
			widget = naughty.container.background
		}
	}
end)


================================================
FILE: src/usr/share/calla/desktop/theme/panel.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi
local dock = require("theme.dock")

local function create(s)

local menu = button {
	image = "calla",
	run = function()
		awesome.emit_signal('widget::launcher')
	end
}

local tray = wibox.widget {
	wibox.widget.systray(),
	margins = 0,
	visible = false,
	widget = wibox.container.margin
}

local systraybutton = hovercursor(wibox.widget {
	buttons = { 
		awful.button({}, 1, function()
			awesome.emit_signal("widget::systray")
		end)
	},
	align = "center",
	widget = iconbox({ image = "left" })
})

local systray = wibox.widget {
	{
		{
			systraybutton,
			forced_width = dpi(30),
			widget = wibox.container.background
		},
		tray,
		layout = wibox.layout.fixed.horizontal
	},
	widget = background({ bg = "bgmid" })
}

local closed = true

local systraystore
awesome.connect_signal("widget::systray", function()
	if closed == true then
		tray.visible = true
		tray.margins = dpi(5)
		systraystore = "right"
		closed = false
	else
		tray.visible = false
		tray.margins = 0
		systraystore = "left"
		closed = true
	end
	systraybutton.image = createicon(systraystore)
end)

local media = wibox.widget {
	{
		{
			{
				id = "icon",
				widget = iconbox({ image = "musicon" })
			},
			{
				id = "title",
				widget = wibox.widget.textbox("Not Playing - No Artist")
			},
			spacing = dpi(4),
			layout = wibox.layout.fixed.horizontal
		},
		left = dpi(8),
		right = dpi(8),
		widget = wibox.container.margin
	},
	widget = background({ bg = "bgmid", fg = "fg" })
}

local playerstore
awesome.connect_signal("signal::playerctl", function(title, album, artist, cover, status)
	if string.len(title .. " - " .. artist) > 50 then
		media:get_children_by_id("title")[1].text = title
	else
		media:get_children_by_id("title")[1].text = title .. " - " .. artist
	end
	if title == "Not Playing" then
		state = false
		playerstore = "musicoff"
	else
		state = true
		playerstore = "musicon"
	end
	media:get_children_by_id("icon")[1].image = createicon(playerstore)
end)

local volumepercent = wibox.widget {
	text = "N/A",
	widget = wibox.widget.textbox
}

local volumeicon = iconbox({ image = "volumemute" })

local volume = wibox.widget {
	{
		{
			volumeicon,
			volumepercent,
			spacing = dpi(4),
			layout = wibox.layout.fixed.horizontal
		},
		left = dpi(8),
		right = dpi(8),
		widget = wibox.container.margin
	},
	widget = background({ bg = "bgmid", fg = "fg" })
}

local volumestore
awesome.connect_signal("signal::volume", function(volume, mute)
	if mute then
		volumepercent.text = "Muted"
		volumestore = "volumemute"
	else
		volumepercent.text = tostring(volume) .. "%"
		if volume > 100 then
			volumestore = "volumewarn"
		elseif volume >= 50 then
			volumestore = "volume100"
		elseif volume >= 25 then
			volumestore = "volume50"
		elseif volume > 0 then
			volumestore = "volume25"
		elseif volume == 0 then
			volumestore = "volume0"
		end
	end
	volumeicon.image = createicon(volumestore)
end)

local batterypercent = wibox.widget {
	text = "N/A",
	widget = wibox.widget.textbox
}

local batteryicon = iconbox({ image = "batterynone" })

local battery = wibox.widget {
	{
		{
			batteryicon,
			batterypercent,
			spacing = dpi(4),
			layout = wibox.layout.fixed.horizontal
		},
		left = dpi(8),
		right = dpi(8),
		widget = wibox.container.margin
	},
	widget = background({ bg = "bgmid", fg = "fg" })
}

local batterystore
if user.batt ~= nil then
	awful.widget.watch("cat /sys/class/power_supply/" .. user.batt .. "/capacity", 15, function(widget, stdout)
		percent = tonumber(stdout)
		batterypercent.text = percent .. "%"
		if percent > 80 then
			batterystore = "battery100"
		elseif percent > 50 then
			batterystore = "battery80"
		elseif percent > 25 then
			batterystore = "battery50"
		elseif percent > 10 then
			batterystore = "battery25"
		elseif percent > 5 then
			batterystore = "battery10"
		else
			batterystore = "battery0"
		end
		batteryicon.image = createicon(batterystore)
	end)
end

local clock = wibox.widget {
	{
		{
			iconbox({ image = "clock" }),
			wibox.widget.textclock('%I:%M %p'),
			spacing = dpi(4),
			layout = wibox.layout.fixed.horizontal
		},
		left = dpi(8),
		right = dpi(8),
		widget = wibox.container.margin
	},
	widget = background({ bg = "bgmid", fg = "fg" })
}

local taglist = awful.widget.taglist {
	screen = s,
	filter = awful.widget.taglist.filter.selected,
	style = {
		shape = function(cr, width, height)
					gears.shape.rounded_rect(cr, width, height, dpi(10))
				end
	},
	widget_template = {
		{
			{
				{
					wibox.widget.textbox("Workspace "),
					widget = background({ fg = "fg" })
				},
				{
					id = "text_role",
					widget = wibox.widget.textbox
				},
				layout = wibox.layout.fixed.horizontal
			},
			left = dpi(8),
			right = dpi(8),
			widget = wibox.container.margin
		},
		id = "background_role",
		widget = wibox.container.background,
		create_callback = function(self)
			hovercursor(self)
		end
	},
	buttons = {
		awful.button({ }, 1, function()
			awesome.emit_signal("widget::preview")
		end),
		awful.button({ }, 4, function(t)
			awful.tag.viewnext(t.screen)
		end),
		awful.button({ }, 5, function(t)
			awful.tag.viewprev(t.screen)
		end)
	}
}

local layouts = awful.widget.layoutbox {
	screen  = s,
	buttons = {
		awful.button({ }, 1, function () awful.layout.inc( 1) end),
		awful.button({ }, 3, function () awful.layout.inc(-1) end),
	}
}

local layoutbox = hovercursor(wibox.widget {
	{
		layouts,
		margins = dpi(5),
		widget = wibox.container.margin
	},
	widget = background({ bg = "bgmid" })
})

awesome.connect_signal("live::reload", function()
	if systraystore then systraybutton.image = createicon(systraystore) end
	if playerstore then media:get_children_by_id("icon")[1].image = createicon(playerstore) end
	if volumestore then volumeicon.image = createicon(volumestore) end
	if batterystore then batteryicon.image = createicon(batterystore) end
	taglist._do_taglist_update_now()
	tag.emit_signal("property::layout", awful.screen.focused().selected_tag)
end)

return wibox.widget {
	{
		{
			{
				menu,
				taglist,
				layoutbox,
				spacing = dpi(5),
				layout = wibox.layout.fixed.horizontal
			},
			nil,
			{
				systray,
				hovercursor(wibox.widget {
					media,
					volume,
					battery,
					clock,
					buttons = {
						awful.button({ }, 1, function()
							awesome.emit_signal("widget::control")
						end)
					},
					spacing = dpi(5),
					layout = wibox.layout.fixed.horizontal
				}),
				spacing = dpi(5),
				layout = wibox.layout.fixed.horizontal
			},
			layout = wibox.layout.align.horizontal
		},
		{
			dock,
			halign = "center",
			widget = wibox.container.place
		},
		layout = wibox.layout.stack
	},
	forced_height = dpi(50),
	margins = dpi(10),
	widget = wibox.container.margin
}

end

return create


================================================
FILE: src/usr/share/calla/desktop/theme/preview.lua
================================================
local awful = require("awful")
local wibox = require("wibox")
local gears = require("gears")
local beautiful = require("beautiful")
local dpi = beautiful.xresources.apply_dpi
local scale = 0.15

local function createpreview(t, s, geometry)
    local clientlayout = wibox.layout.manual()
    clientlayout.forced_height = geometry.height
    clientlayout.forced_width = geometry.width
    for _, c in ipairs(t:clients()) do
        if not c.hidden and not c.minimized then

            local imagebox = wibox.widget {
                resize = true,
                forced_height = dpi(150) * scale,
                forced_width = dpi(150) * scale,
                widget = wibox.widget.imagebox
            }

			if not pcall(function() imagebox.image = gears.surface.load(c.icon) end) then
				imagebox.image = beautiful.calla
			end

            local clientbox = wibox.widget({
                {
                    nil,
                    {
                        nil,
                        imagebox,
                        nil,
                        expand = "outside",
                        layout = wibox.layout.align.horizontal,
                    },
                    nil,
                    expand = "outside",
                    widget = wibox.layout.align.vertical,
                },
                forced_height = math.floor(c.height * scale),
                forced_width = math.floor(c.width * scale),
				border_width = dpi(2),
				border_color = beautiful.bgmid,
                widget = background({ bg = "bg" })
            })

            clientbox.point = {
                x = math.floor((c.x - geometry.x) * scale),
                y = math.floor((c.y - geometry.y) * scale),
            }

            clientlayout:add(clientbox)
        end
    end

	if t:clients()[1] == nil then
		return wibox.widget {
			{
				{
					{
						{
							wibox.widget.textbox("Empty"),
							top = dpi(5),
							bottom = dpi(5),
							left = dpi(8),
							right = dpi(8),
							widget = wibox.container.margin
						},
						widget = background({ bg = "bg", fg = "fg" })
					},
					valign = "center",
					halign = "center",
					widget = wibox.container.place
				},
				bg = beautiful.bg.."96",
				widget = wibox.container.background
			},
			widget = background({ bg = "bgmid" })
		}
	else
		return wibox.widget {
			{
				clientlayout,
				forced_height = geometry.height,
				forced_width = geometry.width,
				widget = wibox.container.place
			},
			widget = background({ bg = "bgmid" })
		}
	end
end

local previewbox = wibox {
	width = dpi(100),
	height = dpi(100),
	ontop = true,
	visible = false,
	widget = live(wibox.container.background, { bg = "bg" })
}

local previewlist = wibox.widget {
	expand = true,
	orientation = "horizontal",
	spacing = dpi(5),
	layout = wibox.layout.grid
}

awesome.connect_signal("widget::preview", function()
	awesome.emit_signal("widget::launcher:hide")

	if previewbox.visible then
		previewbox.visible = false
		return
	end

	previewlist:reset()

	local geometry = awful.screen.focused():get_bounding_geometry()
	local tags = awful.screen.focused().tags
	local numtags

	for i, tag in ipairs(tags) do
		numtags = i

		local preview = wibox.widget {
			hovercursor(createpreview(tag, tag.screen, geometry)),
			buttons = {
				awful.button({}, 1, function()
					awesome.emit_signal("widget::preview")
					tag:view_only()
				end)
			},
			widget = wibox.container.background
		}

		previewlist:add(preview)
	end

	previewbox.width = geometry.width * scale * numtags + (numtags + 1) * dpi(5)
	previewbox.height = (geometry.height-dpi(40)) * scale + (2 * dpi(5))
	previewbox.widget = wibox.widget {
			{
			previewlist,
			margins = dpi(5),
			widget = wibox.container.margin
		},
		widget = live(wibox.container.background, { bg = "bg" })
	}

	awful.placement.bottom_left(
		previewbox,
		{
			margins = {
				bottom = dpi(60),
				left = dpi(20),
			},
			parent = awful.screen.focused()
		}
	)

	previewbox.visible = true
end)

awesome.connect_signal("widget::preview:hide", function()
	previewbox.visible = false
end)


================================================
FILE: src/usr/share/calla/desktop/theme/settings.lua
================================================
awesome.connect_signal("widget::config", function()

local dpi = require("beautiful").xresources.apply_dpi
local lgi = require("lgi")
local Gio = lgi.Gio
local Gtk = lgi.require("Gtk", "3.0")
local Gdk = lgi.require("Gdk", "3.0")
local GdkPixbuf = lgi.require("GdkPixbuf", "2.0")
local GObject = lgi.require("GObject", "2.0")

local appID = "io.github.stardust-kyun.calla.settings"
local appTitle = "Settings"
local app = Gtk.Application({ application_id = appID })

local function copytable(table)
	local copy = {}
	for key, value in pairs(table) do
		copy[key] = value
	end
	return copy
end

local settings = copytable(user)

local color

local function genColor()
	color = readjson(require("gears").filesystem.get_configuration_dir() .. "color/" .. settings.color .. "/" .. settings.color .. ".json")
end

genColor()

local function settingsEntry(name, setting, default)

	local label = Gtk.Label({ label = name, halign = Gtk.Align.START })
	local settingsEntry = Gtk.Entry({ placeholder_text = settings[setting], width = dpi(175) })
	function settingsEntry:on_key_release_event()
		settings[setting] = settingsEntry.text
	end
	local button = Gtk.Button.new_with_label("Default")
	function button:on_clicked()
		settings[setting] = default
		settingsEntry.placeholder_text = settings[setting]
		settingsEntry.text = ""
	end
	local grid = Gtk.Grid({
		column_spacing = dpi(10),
		row_spacing = dpi(10),
		halign = Gtk.Align.CENTER,

		{ label, top_attach = 0, left_attach = 0, width = 2 },
		{ settingsEntry, top_attach = 1, left_attach = 0 },
		{ button, top_attach = 1, left_attach = 1 },
	})

	return grid

end

local function colorEntry(name, setting, default)

	local label = Gtk.Label({ label = name, halign = Gtk.Align.START })
	local colorEntry = Gtk.Entry({ placeholder_text = color[setting], width = dpi(175) })
	function colorEntry:on_key_release_event()
		color[setting] = colorEntry.text
	end
	local button = Gtk.Button.new_with_label("Default")
	function button:on_clicked()
		color[setting] = default
		colorEntry.placeholder_text = color[setting]
		colorEntry.text = ""
	end
	local grid = Gtk.Grid({
		column_spacing = dpi(10),
		row_spacing = dpi(10),
		halign = Gtk.Align.CENTER,

		{ label, top_attach = 0, left_attach = 0, width = 2 },
		{ colorEntry, top_attach = 1, left_attach = 0 },
		{ button, top_attach = 1, left_attach = 1 },
	})

	return grid

end

local function createColor(name, col, colname)

	local rgbcol = Gdk.RGBA.parse(col)

	local label = Gtk.Label({ label = name, halign = Gtk.Align.START })
	local button = Gtk.ColorButton({ rgba = rgbcol, show_editor = true })
	function button:on_color_set()
		local out = button:get_rgba():to_string()

		local red = out:match("%d+,"):gsub(",", "")
		local green = out:match(",%d+,"):gsub(",", "")
		local blue = out:match(",%d+%)"):gsub(",", ""):gsub("%)", "")

		local hex = string.format("#%02X%02X%02X", red, green, blue)
		
		color[colname] = hex
	end
	local grid = Gtk.Grid({
		column_spacing = dpi(10),
		row_spacing = dpi(10),
		halign = Gtk.Align.CENTER,

		{ label, top_attach = 0, left_attach = 0 },
		{ button, top_attach = 1, left_attach = 0 },
	})

	return grid

end

local function doGeneral()

	local function wallpaper()

		local label = Gtk.Label({ label = "Wallpaper", halign = Gtk.Align.START })

		local filter = Gtk.FileFilter()
		filter:add_pattern("*.png")
		filter:add_pattern("*.jpg")
		filter:add_pattern("*.JPG")
		filter:add_pattern("*.jpeg")

		if settings.wallpaper ~= nil then
			local pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(settings.wallpaper:gsub("~", os.getenv("HOME")), 500, 500, true)
		else
			local pixbuf
		end
		local preview = Gtk.Image()

		local wallpaper = Gtk.FileChooserButton({ filter = filter, title = "Choose Wallpaper", width = dpi(175), preview_widget = preview })
		if settings.wallpaper ~= nil then
			wallpaper:set_file(Gio.File.new_for_path(settings.wallpaper:gsub("~", os.getenv("HOME"))))
			wallpaper:set_current_folder_file(Gio.File.new_for_path(settings.wallpaper:gsub("~", os.getenv("HOME"))))
		else
			wallpaper:set_file(Gio.File.new_for_path(""))
			wallpaper:set_current_folder_file(Gio.File.new_for_path(os.getenv("HOME")))
		end
		function wallpaper:on_file_set()
			settings.wallpaper = self:get_filename():gsub(os.getenv("HOME"), "~")
		end
		function wallpaper:on_update_preview()
			pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self:get_preview_filename(), 500, 500, true)
			preview:set_from_pixbuf(pixbuf)
			wallpaper:set_preview_widget_active(true)
		end

		local button = Gtk.Button.new_with_label("Default")
		function button:on_clicked()
			settings.wallpaper = nil
			wallpaper:set_file(Gio.File.new_for_path(""))
		end

		local grid = Gtk.Grid({
			column_spacing = dpi(10),
			row_spacing = dpi(10),
			halign = Gtk.Align.CENTER,

			{ label, top_attach = 0, left_attach = 0, width = 2 },
			{ wallpaper, top_attach = 1, left_attach = 0 },
			{ button, top_attach = 1, left_attach = 1 },
		})

		return grid

	end

	local function screenshot()

		local label = Gtk.Label({ label = "Screenshot Directory", halign = Gtk.Align.START })
		local button = Gtk.FileChooserButton({ title = "Choose Screenshot Directory", width = dpi(245), action = Gtk.FileChooserAction.SELECT_FOLDER })
		button:set_current_folder_file(Gio.File.new_for_path(settings.shotdir:gsub("~", os.getenv("HOME"))))
		function button:on_file_set()
			settings.shotdir = self:get_filename():gsub(os.getenv("HOME"), "~")
		end

		local grid = Gtk.Grid({
			column_spacing = dpi(10),
			row_spacing = dpi(10),
			halign = Gtk.Align.CENTER,

			{ label, top_attach = 0, left_attach = 0 },
			{ button, top_attach = 1, left_attach = 0 },
		})

		return grid

	end

	local grid = Gtk.Grid({
		column_spacing = dpi(20),
		row_spacing = dpi(10),
		margin = dpi(20),
		halign = Gtk.Align.CENTER,

		{ settingsEntry("Terminal", "terminal", "st"), top_attach = 0, left_attach = 0 },
		{ settingsEntry("Shutdown", "shutdown", "systemctl poweroff"), top_attach = 1, left_attach = 0 },
		{ settingsEntry("Reboot", "reboot", "systemctl reboot"), top_attach = 2, left_attach = 0 },
		{ settingsEntry("Font", "font", "Roboto Medium 11"), top_attach = 0, left_attach = 1 },
		{ settingsEntry("Alt Font", "fontalt", "Roboto Bold 11"), top_attach = 1, left_attach = 1 },
		{ settingsEntry("Fallback Password", "passwd", "awesomewm"), top_attach = 3, left_attach = 0 },
		{ settingsEntry("Battery", "batt", "BAT0"), top_attach = 2, left_attach = 1 },
		{ wallpaper(), top_attach = 3, left_attach = 1 },
		{ settingsEntry("temp", "temp", "temp"), top_attach = 4, left_attach = 1 },
		{ screenshot(), top_attach = 4, left_attach = 0 },
	})

	return grid

end

local function doColor()

	genColor()

	local left = Gtk.Grid({
		column_spacing = dpi(10),
		row_spacing = dpi(10),
		halign = Gtk.Align.CENTER,

		{ createColor("Bg", color.bg, "bg"), top_attach = 0, left_attach = 0 },
		{ createColor("Mid Bg", color.bgmid, "bgmid"), top_attach = 1, left_attach = 0 },
		{ createColor("Alt Bg", color.bgalt, "bgalt"), top_attach = 2, left_attach = 0 },
		{ createColor("Fg", color.fg, "fg"), top_attach = 0, left_attach = 1 },
		{ createColor("Black", color.black, "black"), top_attach = 1, left_attach = 1 },
		{ createColor("White", color.white, "white"), top_attach = 2, left_attach = 1 },
		{ createColor("Red", color.red, "red"), top_attach = 0, left_attach = 2 },
		{ createColor("Green", color.green, "green"), top_attach = 1, left_attach = 2 },
		{ createColor("Yellow", color.yellow, "yellow"), top_attach = 2, left_attach = 2 },
		{ createColor("Blue", color.blue, "blue"), top_attach = 0, left_attach = 3 },
		{ createColor("Magenta", color.magenta, "magenta"), top_attach = 1, left_attach = 3 },
		{ createColor("Cyan", color.cyan, "cyan"), top_attach = 2, left_attach = 3 },

		{ colorEntry("Gui Theme", "gtk", color.gtk), top_attach = 3, left_attach = 0, width = 4 },
	})

	local right = Gtk.Grid({
		column_spacing = dpi(10),
		row_spacing = dpi(10),
		halign = Gtk.Align.CENTER,

		{ colorEntry("Compositor Radius", "compradius", color.compradius), top_attach = 0, left_attach = 0 },
		{ colorEntry("Compositor Offset", "compoffset", color.compoffset), top_attach = 1, left_attach = 0 },
		{ colorEntry("Compositor Opacity", "compopacity", color.compopacity), top_attach = 2, left_attach = 0 },
		{ colorEntry("Icon Theme", "icons", color.icons), top_attach = 3, left_attach = 0 },
	})

	local grid = Gtk.Grid({
		column_spacing = dpi(20),
		halign = Gtk.Align.CENTER,

		{ left, top_attach = 0, left_attach = 0 },
		{ right, top_attach = 0, left_attach = 1 },

	})

	return grid
end

local function doTheme()

	local function colorgen()
		local colors = {}

		for dir in io.popen("ls -d " .. require("gears").filesystem.get_configuration_dir() .. "color/*/ | rev | cut -f2 -d'/' | rev"):lines() do
			table.insert(colors, dir)
		end

		return colors
	end

	local model = Gtk.ListStore.new({ GObject.Type.STRING })
	local items = colorgen()
	local currentcolor = 0

	for i, name in ipairs(items) do
		model:append({ name })

		if name == settings.color then
			currentcolor = i-1
		end
	end

	local combo = Gtk.ComboBox({
		model = model,
		active = currentcolor,
		cells = {
			{
				Gtk.CellRendererText(),
				{ text = 1 },
				align = Gtk.Align.START
			}
		},
		expand = true
	})

	local savebutton = Gtk.Button.new_with_label("Save Theme As...")
	local confirmbutton = Gtk.Button.new_with_label("Confirm")
	local cancelbutton = Gtk.Button.new_with_label("Cancel")
	local nameset = false

	local themeprompt = Gtk.Label({ label = "What should this theme be called?", halign = Gtk.Align.START })
	local themename = Gtk.Entry({ placeholder_text = "Theme Name", hexpand = true })
	local name
	function themename:on_key_release_event()
		name = themename.text
	end

	local wallprompt = Gtk.Label({ label = "What wallpaper should this theme use?", halign = Gtk.Align.START })
	local filter = Gtk.FileFilter()
	filter:add_pattern("*.png")
	filter:add_pattern("*.jpg")

	local preview = Gtk.Image()

	local wallpaper = nil
	local wallpaperbutton = Gtk.FileChooserButton({ filter = filter, title = "Choose Wallpaper", hexpand = true, preview_widget = preview })
	wallpaperbutton:set_file(Gio.File.new_for_path(""))
	wallpaperbutton:set_current_folder_file(Gio.File.new_for_path(os.getenv("HOME")))
	function wallpaperbutton:on_file_set()
		wallpaper = self:get_filename()
	end
	function wallpaperbutton:on_update_preview()
		pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(self:get_preview_filename(), 500, 500, true)
		preview:set_from_pixbuf(pixbuf)
		wallpaperbutton:set_preview_widget_active(true)
	end

	local colorscheme = Gtk.Grid({
		column_spacing = dpi(10),
		row_spacing = dpi(10),

		{ Gtk.Label({ label = "Color Scheme", halign = Gtk.Align.START }), top_attach = 0, left_attach = 0 },
		{ combo, top_attach = 1, left_attach = 0 },
		{ savebutton, top_attach = 1, left_attach = 1 }
	})

	function savebutton:on_clicked()
		colorscheme:remove(combo)
		colorscheme:remove(savebutton)
		colorscheme:attach(themeprompt, 0, 1, 1, 1)
		colorscheme:attach(themename, 1, 1, 1, 1)
		colorscheme:attach(confirmbutton, 2, 1, 1, 1)
		colorscheme:attach(cancelbutton, 3, 1, 1, 1)
		colorscheme:show_all()
	end
	function confirmbutton:on_clicked()
		if nameset == false and name ~= "" then
			nameset = true
			colorscheme:remove(themeprompt)
			colorscheme:remove(themename)
			colorscheme:attach(wallprompt, 0, 1, 1, 1)
			colorscheme:attach(wallpaperbutton, 1, 1, 1, 1)
			colorscheme:show_all()	
		elseif nameset == true and wallpaper then
			local themedir = require("gears").filesystem.get_configuration_dir() .. "color/" .. name .. "/"
			local path = Gio.File.new_for_path(themedir)
			local sourcepath = Gio.File.new_for_path(wallpaper)
			local targetpath = Gio.File.new_for_path(themedir .. name .. ".png")
			path:make_directory()
			sourcepath:copy(targetpath, Gio.FileCopyFlags.NONE, nil, nil, nil, nil)
			color.wall = targetpath:get_path():gsub(require("gears").filesystem.get_configuration_dir(), "")
			writejson(require("gears").filesystem.get_configuration_dir() .. "color/" .. name .. "/" .. name .. ".json", color)
		
			table.insert(items, name)
			model:append({name})
			colorscheme:remove(wallprompt)
			colorscheme:remove(wallpaperbutton)
			colorscheme:remove(confirmbutton)
			colorscheme:remove(cancelbutton)
			colorscheme:attach(combo, 0, 1, 1, 1)
			colorscheme:attach(savebutton, 1, 1, 1, 1)
			nameset = false
			themename.text = ""
			colorscheme:show_all()
		end
	end
	function cancelbutton:on_clicked()
		colorscheme:remove(themeprompt)
		colorscheme:remove(themename)
		colorscheme:remove(wallprompt)
		colorscheme:remove(wallpaperbutton)
		colorscheme:remove(confirmbutton)
		colorscheme:remove(cancelbutton)
		colorscheme:attach(combo, 0, 1, 1, 1)
		colorscheme:attach(savebutton, 1, 1, 1, 1)
		nameset = false
		themename.text = ""
		colorscheme:show_all()
	end

	local grid = Gtk.Grid({
		column_spacing = dpi(10),
		row_spacing = dpi(10),
		margin = dpi(20),
		halign = Gtk.Align.CENTER,

		{ colorscheme, top_attach = 0, left_attach = 0 },
		{ doColor(), top_attach = 1, left_attach 
Download .txt
gitextract_yiu3gkuy/

├── .gitignore
├── DEBIAN/
│   └── control
├── PKGBUILD
├── README.md
├── build.sh
└── src/
    └── usr/
        ├── bin/
        │   └── calla
        └── share/
            ├── calla/
            │   ├── Xresources
            │   ├── compositor.conf
            │   ├── desktop/
            │   │   ├── color/
            │   │   │   ├── .backup/
            │   │   │   │   ├── bloom/
            │   │   │   │   │   └── bloom.json
            │   │   │   │   ├── dark/
            │   │   │   │   │   ├── dark.json
            │   │   │   │   │   └── dark.json.bak
            │   │   │   │   ├── light/
            │   │   │   │   │   └── light.json
            │   │   │   │   ├── sakura/
            │   │   │   │   │   └── sakura.json
            │   │   │   │   ├── shore/
            │   │   │   │   │   └── shore.json
            │   │   │   │   └── wave/
            │   │   │   │       └── wave.json
            │   │   │   ├── dark/
            │   │   │   │   └── dark.json
            │   │   │   ├── desktop.lua
            │   │   │   ├── light/
            │   │   │   │   └── light.json
            │   │   │   ├── solarized/
            │   │   │   │   └── solarized.json
            │   │   │   └── terminal.sh
            │   │   ├── config/
            │   │   │   ├── bind.lua
            │   │   │   ├── init.lua
            │   │   │   ├── main.lua
            │   │   │   ├── rule.lua
            │   │   │   └── shot.lua
            │   │   ├── json.lua
            │   │   ├── rc.lua
            │   │   ├── signal/
            │   │   │   ├── brightness.lua
            │   │   │   ├── desktop.lua
            │   │   │   ├── init.lua
            │   │   │   ├── playerctl.lua
            │   │   │   └── volume.lua
            │   │   └── theme/
            │   │       ├── brightness.lua
            │   │       ├── control/
            │   │       │   ├── calendar.lua
            │   │       │   ├── init.lua
            │   │       │   ├── media.lua
            │   │       │   ├── notifs.lua
            │   │       │   ├── profile.lua
            │   │       │   ├── sliders.lua
            │   │       │   ├── system.lua
            │   │       │   └── toggles.lua
            │   │       ├── desktop.lua
            │   │       ├── dock.lua
            │   │       ├── init.lua
            │   │       ├── launcher.lua
            │   │       ├── lock.lua
            │   │       ├── notif.lua
            │   │       ├── panel.lua
            │   │       ├── preview.lua
            │   │       ├── settings.lua
            │   │       ├── theme.lua
            │   │       ├── title.lua
            │   │       └── volume.lua
            │   └── xsettingsd
            ├── themes/
            │   ├── dark/
            │   │   ├── gtk-3.0/
            │   │   │   └── gtk.css
            │   │   └── index.theme
            │   ├── light/
            │   │   ├── gtk-3.0/
            │   │   │   └── gtk.css
            │   │   └── index.theme
            │   └── solarized/
            │       ├── gtk-3.0/
            │       │   └── gtk.css
            │       └── index.theme
            └── xsessions/
                └── calla.desktop
Condensed preview — 61 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (291K chars).
[
  {
    "path": ".gitignore",
    "chars": 27,
    "preview": "pkg/\n*.tar.*\n*.deb\npackage/"
  },
  {
    "path": "DEBIAN/control",
    "chars": 520,
    "preview": "Package: calla\nVersion: 0.3.0-5\nMaintainer: Stella <stardust-kyun@proton.me>\nDepends: awesome (>=4.3.0.0~git), xorg, pip"
  },
  {
    "path": "PKGBUILD",
    "chars": 949,
    "preview": "# From @levraiardox \n# To https://github.com/Stardust-kyun/calla\n\npkgname=\"calla\"\npkgver=\"0.3\"\npkgrel=\"1\"\npkgdesc=\"Calla"
  },
  {
    "path": "README.md",
    "chars": 4327,
    "preview": "<h1 align=center>Calla</h1>\n\n<div align=\"center\">\n<a href=\"#install\">Install</a> - <a href=\"#usage\">Usage</a> - <a href="
  },
  {
    "path": "build.sh",
    "chars": 685,
    "preview": "#!/usr/bin/env bash\nurl=`cat ./.git/config | grep \"url = \" | sed \"s/^[^=]*= //\"`\nif [[ $url == \"https://github.com/stard"
  },
  {
    "path": "src/usr/bin/calla",
    "chars": 92,
    "preview": "#!/bin/sh\nxrdb /usr/share/calla/Xresources\nawesome --config /usr/share/calla/desktop/rc.lua\n"
  },
  {
    "path": "src/usr/share/calla/Xresources",
    "chars": 660,
    "preview": "#define BG #f5f5f5\n#define FG #303030\n#define BL #505050\n#define WH #d0d0d0\n#define R #ac4142\n#define G #90a959\n#define "
  },
  {
    "path": "src/usr/share/calla/compositor.conf",
    "chars": 1250,
    "preview": "#################################\n#\n# Shadows\n#\n#################################\n\n# Enabled client-side shadows on wind"
  },
  {
    "path": "src/usr/share/calla/desktop/color/.backup/bloom/bloom.json",
    "chars": 383,
    "preview": "{\n\t\"bg\": \"#fffaf5\",\n\t\"bgalt\": \"#ebe6e1\",\n\t\"bgmid\": \"#ebe6e1\",\n\t\"black\": \"#4b4646\",\n\t\"blue\": \"#9bb9f0\",\n\t\"compoffset\": \"-"
  },
  {
    "path": "src/usr/share/calla/desktop/color/.backup/dark/dark.json",
    "chars": 386,
    "preview": "{\n\t\"bg\": \"#151515\",\n\t\"bgalt\": \"#1a1a1a\",\n\t\"bgmid\": \"#202020\",\n\t\"black\": \"#505050\",\n\t\"blue\": \"#6a9fb5\",\n\t\"compoffset\": \"-"
  },
  {
    "path": "src/usr/share/calla/desktop/color/.backup/dark/dark.json.bak",
    "chars": 382,
    "preview": "{\n\t\"bg\": \"#151515\",\n\t\"bgalt\": \"#252525\",\n\t\"bgmid\": \"#1d1d1d\",\n\t\"black\": \"#505050\",\n\t\"blue\": \"#6a9fb5\",\n\t\"compoffset\": \"-"
  },
  {
    "path": "src/usr/share/calla/desktop/color/.backup/light/light.json",
    "chars": 445,
    "preview": "{\n    \"bg\": \"#f5f5f5\",\n    \"bgalt\": \"#d0d0d0\",\n    \"bgmid\": \"#e3e3e3\",\n    \"black\": \"#505050\",\n    \"blue\": \"#6a9fb5\",\n  "
  },
  {
    "path": "src/usr/share/calla/desktop/color/.backup/sakura/sakura.json",
    "chars": 387,
    "preview": "{\n\t\"bg\": \"#000f14\",\n\t\"bgalt\": \"#0a191e\",\n\t\"bgmid\": \"#0a191e\",\n\t\"black\": \"#0a191e\",\n\t\"blue\": \"#326482\",\n\t\"compoffset\": \"-"
  },
  {
    "path": "src/usr/share/calla/desktop/color/.backup/shore/shore.json",
    "chars": 383,
    "preview": "{\n\t\"bg\": \"#19191e\",\n\t\"bgalt\": \"#2b2b33\",\n\t\"bgmid\": \"#2b2b33\",\n\t\"black\": \"#2b2b33\",\n\t\"blue\": \"#505a82\",\n\t\"compoffset\": \"-"
  },
  {
    "path": "src/usr/share/calla/desktop/color/.backup/wave/wave.json",
    "chars": 379,
    "preview": "{\n\t\"bg\": \"#f0fafa\",\n\t\"bgalt\": \"#dce6e6\",\n\t\"bgmid\": \"#dce6e6\",\n\t\"black\": \"#404040\",\n\t\"blue\": \"#83b4e6\",\n\t\"compoffset\": \"-"
  },
  {
    "path": "src/usr/share/calla/desktop/color/dark/dark.json",
    "chars": 386,
    "preview": "{\n\t\"bg\": \"#151515\",\n\t\"bgalt\": \"#2B2B2B\",\n\t\"bgmid\": \"#202020\",\n\t\"black\": \"#505050\",\n\t\"blue\": \"#6a9fb5\",\n\t\"compoffset\": \"-"
  },
  {
    "path": "src/usr/share/calla/desktop/color/desktop.lua",
    "chars": 2390,
    "preview": "local calla=\"/usr/share/calla/\"\nlocal picom=calla..\"compositor.conf\"\nlocal xresources=calla..\"Xresources\"\nlocal xsetting"
  },
  {
    "path": "src/usr/share/calla/desktop/color/light/light.json",
    "chars": 391,
    "preview": "{\n\t\"bg\": \"#f5f5f5\",\n\t\"bgalt\": \"#d0d0d0\",\n\t\"bgmid\": \"#e3e3e3\",\n\t\"black\": \"#505050\",\n\t\"blue\": \"#6a9fb5\",\n\t\"compoffset\": \"-"
  },
  {
    "path": "src/usr/share/calla/desktop/color/solarized/solarized.json",
    "chars": 402,
    "preview": "{\n\t\"bg\": \"#002B36\",\n\t\"bgalt\": \"#063C4A\",\n\t\"bgmid\": \"#043643\",\n\t\"black\": \"#657B83\",\n\t\"blue\": \"#268BD2\",\n\t\"compoffset\": \"-"
  },
  {
    "path": "src/usr/share/calla/desktop/color/terminal.sh",
    "chars": 552,
    "preview": "#!/usr/bin/env bash\n\nxrdb_query() {\n\tvalue=$(xrdb -query | grep -i \"^*\\.$1:\" | cut -f 2)\n\tif [ -n \"${value}\" ]; then\n\t\te"
  },
  {
    "path": "src/usr/share/calla/desktop/config/bind.lua",
    "chars": 6802,
    "preview": "local awful = require(\"awful\")\nlocal mod = user.mod\n\n-- Mouse \n\nclient.connect_signal(\"request::default_mousebindings\", "
  },
  {
    "path": "src/usr/share/calla/desktop/config/init.lua",
    "chars": 92,
    "preview": "require(\"config.main\")\nrequire(\"config.bind\")\nrequire(\"config.rule\")\nrequire(\"config.shot\")\n"
  },
  {
    "path": "src/usr/share/calla/desktop/config/main.lua",
    "chars": 1771,
    "preview": "local awful = require(\"awful\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\"beautiful\")\n\n-- Manage new wind"
  },
  {
    "path": "src/usr/share/calla/desktop/config/rule.lua",
    "chars": 1203,
    "preview": "local awful = require(\"awful\")\nlocal gears = require(\"gears\")\nlocal ruled = require(\"ruled\")\nlocal beautiful = require(\""
  },
  {
    "path": "src/usr/share/calla/desktop/config/shot.lua",
    "chars": 1166,
    "preview": "local awful = require(\"awful\")\nlocal naughty = require(\"naughty\")\n\nlocal function screenshot(args)\n\n\tlocal tmp = \"/tmp/\""
  },
  {
    "path": "src/usr/share/calla/desktop/json.lua",
    "chars": 69055,
    "preview": "-- -*- coding: utf-8 -*-\n--\n-- Simple JSON encoding and decoding in pure Lua.\n--\n-- Copyright 2010-2017 Jeffrey Friedl\n-"
  },
  {
    "path": "src/usr/share/calla/desktop/rc.lua",
    "chars": 3358,
    "preview": "--[[ \n--\tTODO\n--\n--\tWhy is text content changed when its color is changed? -- markdown? needs to be bg container?\n--\n--\t"
  },
  {
    "path": "src/usr/share/calla/desktop/signal/brightness.lua",
    "chars": 653,
    "preview": "local awful = require(\"awful\")\n\nlocal function emit()\n\tawful.spawn.easy_async_with_shell(\"brightnessctl -m | awk -F, '{p"
  },
  {
    "path": "src/usr/share/calla/desktop/signal/desktop.lua",
    "chars": 908,
    "preview": "local awful = require(\"awful\")\n\nlocal emit = function(type)\n\tawesome.emit_signal(\"signal::desktop\", type) \nend\n\nemit()\n\n"
  },
  {
    "path": "src/usr/share/calla/desktop/signal/init.lua",
    "chars": 108,
    "preview": "require(\"signal.volume\")\nrequire(\"signal.brightness\")\nrequire(\"signal.playerctl\")\nrequire(\"signal.desktop\")\n"
  },
  {
    "path": "src/usr/share/calla/desktop/signal/playerctl.lua",
    "chars": 1300,
    "preview": "local awful = require(\"awful\")\n\nlocal function emit()\n\tawful.spawn.easy_async_with_shell(\"playerctl --player=%any,firefo"
  },
  {
    "path": "src/usr/share/calla/desktop/signal/volume.lua",
    "chars": 791,
    "preview": "local awful = require(\"awful\")\n\nlocal volume_old = -1\nlocal muted_old = -1\nlocal function emit()\n\tawful.spawn.easy_async"
  },
  {
    "path": "src/usr/share/calla/desktop/theme/brightness.lua",
    "chars": 2716,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\""
  },
  {
    "path": "src/usr/share/calla/desktop/theme/control/calendar.lua",
    "chars": 1041,
    "preview": "local wibox = require(\"wibox\")\nlocal beautiful = require(\"beautiful\")\nlocal dpi = beautiful.xresources.apply_dpi\n\nlocal "
  },
  {
    "path": "src/usr/share/calla/desktop/theme/control/init.lua",
    "chars": 2190,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal beautiful = require(\"beautiful\")\nlocal dpi = beautif"
  },
  {
    "path": "src/usr/share/calla/desktop/theme/control/media.lua",
    "chars": 4165,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\""
  },
  {
    "path": "src/usr/share/calla/desktop/theme/control/notifs.lua",
    "chars": 3885,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal naughty = require(\"naughty\")\nlocal beautiful = requi"
  },
  {
    "path": "src/usr/share/calla/desktop/theme/control/profile.lua",
    "chars": 1719,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\""
  },
  {
    "path": "src/usr/share/calla/desktop/theme/control/sliders.lua",
    "chars": 4873,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\""
  },
  {
    "path": "src/usr/share/calla/desktop/theme/control/system.lua",
    "chars": 1260,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal beautiful = require(\"beautiful\")\nlocal dpi = beautif"
  },
  {
    "path": "src/usr/share/calla/desktop/theme/control/toggles.lua",
    "chars": 3978,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal naughty = require(\"na"
  },
  {
    "path": "src/usr/share/calla/desktop/theme/desktop.lua",
    "chars": 10309,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\""
  },
  {
    "path": "src/usr/share/calla/desktop/theme/dock.lua",
    "chars": 8289,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\""
  },
  {
    "path": "src/usr/share/calla/desktop/theme/init.lua",
    "chars": 4003,
    "preview": "local beautiful = require(\"beautiful\")\nlocal awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = requi"
  },
  {
    "path": "src/usr/share/calla/desktop/theme/launcher.lua",
    "chars": 6785,
    "preview": "local wibox = require(\"wibox\")\nlocal awful = require(\"awful\")\nlocal gears = require(\"gears\")\nlocal Gio = require(\"lgi\")."
  },
  {
    "path": "src/usr/share/calla/desktop/theme/lock.lua",
    "chars": 8176,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\""
  },
  {
    "path": "src/usr/share/calla/desktop/theme/notif.lua",
    "chars": 2353,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal ruled = require(\"rule"
  },
  {
    "path": "src/usr/share/calla/desktop/theme/panel.lua",
    "chars": 6918,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\""
  },
  {
    "path": "src/usr/share/calla/desktop/theme/preview.lua",
    "chars": 4096,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\""
  },
  {
    "path": "src/usr/share/calla/desktop/theme/settings.lua",
    "chars": 15883,
    "preview": "awesome.connect_signal(\"widget::config\", function()\n\nlocal dpi = require(\"beautiful\").xresources.apply_dpi\nlocal lgi = r"
  },
  {
    "path": "src/usr/share/calla/desktop/theme/theme.lua",
    "chars": 2536,
    "preview": "local gears = require(\"gears\")\nlocal dpi = require(\"beautiful\").xresources.apply_dpi\nlocal iconpath = require(\"gears\").f"
  },
  {
    "path": "src/usr/share/calla/desktop/theme/title.lua",
    "chars": 3144,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\""
  },
  {
    "path": "src/usr/share/calla/desktop/theme/volume.lua",
    "chars": 2734,
    "preview": "local awful = require(\"awful\")\nlocal wibox = require(\"wibox\")\nlocal gears = require(\"gears\")\nlocal beautiful = require(\""
  },
  {
    "path": "src/usr/share/calla/xsettingsd",
    "chars": 56,
    "preview": "Net/ThemeName \"light\"\nNet/IconThemeName \"Papirus-Light\"\n"
  },
  {
    "path": "src/usr/share/themes/dark/gtk-3.0/gtk.css",
    "chars": 19201,
    "preview": "@define-color borders #151515;\nwindow.background.chromium {\n  background: #262626;\n  color: #d0d0d0;\n}\nwindow.background"
  },
  {
    "path": "src/usr/share/themes/dark/index.theme",
    "chars": 181,
    "preview": "[Desktop Entry]\nType=X-GNOME-Metatheme\nName=dark\nComment=base16-defaultdark\nEncoding=UTF-8\n\n[X-GNOME-Metatheme]\nGtkTheme"
  },
  {
    "path": "src/usr/share/themes/light/gtk-3.0/gtk.css",
    "chars": 19120,
    "preview": "@define-color borders whitesmoke;\nwindow.background.chromium {\n  background: #d5d5d5;\n  color: #303030;\n}\nwindow.backgro"
  },
  {
    "path": "src/usr/share/themes/light/index.theme",
    "chars": 184,
    "preview": "[Desktop Entry]\nType=X-GNOME-Metatheme\nName=light\nComment=base16-defaultlight\nEncoding=UTF-8\n\n[X-GNOME-Metatheme]\nGtkThe"
  },
  {
    "path": "src/usr/share/themes/solarized/gtk-3.0/gtk.css",
    "chars": 19189,
    "preview": "@define-color borders #002b36;\nwindow.background.chromium {\n  background: #074453;\n  color: #93a1a1;\n}\nwindow.background"
  },
  {
    "path": "src/usr/share/themes/solarized/index.theme",
    "chars": 182,
    "preview": "[Desktop Entry]\nType=X-GNOME-Metatheme\nName=solarized\nComment=solarized\nEncoding=UTF-8\n\n[X-GNOME-Metatheme]\nGtkTheme=sol"
  },
  {
    "path": "src/usr/share/xsessions/calla.desktop",
    "chars": 95,
    "preview": "[Desktop Entry]\nName=Calla\nComment=Calla Desktop Environment\nExec=calla\nIcon=\nType=Application\n"
  }
]

About this extraction

This page contains the full source code of the Stardust-kyun/calla GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 61 files (255.7 KB), approximately 77.1k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!