feat!: add Config Swap plugin (#112)

* feat!: init plugin

* fix: fix widget not found on bar setting

* feat!: add creation folder for plugin

* feat!: add first version of Config Swap (simple modification test)

* feat!: update toml with new feature

* docs: prepare docs

* feat!: add more place for view preview

* feat!: add new page for store and allerady download

* docs: improve lisibility

* feat!: add description emplacement

* feat!: add wallpaper support for install and set

* docs: add more info to readme

* feat: add cp depedencie

* docs: add more info

* feat!: add trad support

* fix: change order for set wallpaper after change config

* feat!: add settingsPath for data.json

* feat!: add data.json creation if not exist

* docs: add more docs info

* feat!: rm useless preview image

* docs: improve docs

* feat!: change false description

* feat!: add shellQuote for protect bad command

* feat!: add path var in infoConfig.Change config type for infoConfig Type

* feat!: add secure delete config and confirmation for delete config

* feat!: add save function for save current config

* feat!: add welcome warning and confirmation switch

* feat!: add info for store section

* docs: add new info in setting section

* feat!: add resolve state dir for check all state dir possibility

* feat!: add english translation compatibility

* feat!: change thumbnail

* docs: add more information on the readme

* fix: rm useless button

* docs: add comment

* fix: add depedencie missing

* fix: rm death code

* fix: rm update interval not use

* feat!: add delete, save and apply protection, reject invalid name

* docs: add more info in info section

* feat!: add more charactere in match string in isDetect function

* fix: change wrong name function

* feat!: add protection for invalid name in dowload Config
This commit is contained in:
Tadomika-Ari
2026-08-06 10:43:14 -04:00
committed by GitHub
parent ed480cc21c
commit b6b141ecec
7 changed files with 864 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
# Config Swap
A [Noctalia](https://github.com/noctalia-dev/noctalia) v5 plugin for switching between saved Noctalia configurations.
## Dependencies
For this plugin, you need:
* internet access to fetch configuration data from GitHub on the store page
* `cp` to apply a configuration
* `rm` to delete the selected installed configuration (not the current configuration)
## Plugin
| Field | Value |
| --- | --- |
| ID | `tadomika_ari/config-swap` |
| Entries | Bar widget: `widget`; panel: `panel`; service: `start` |
## Usage
This plugin is still under development.
You can open Config Swap with:
```sh
noctalia msg panel-toggle tadomika_ari/config-swap:panel
```
The plugin downloads saved configurations from the `Config-Swap-Box` GitHub repository and stores them in `~/Config-Swap`.
Applying a configuration copies its `settings.toml` to `~/.local/state/noctalia/settings.toml` and sets the matching wallpaper.
Keep a backup of your current configuration before using it.
Open the panel from the bar widget, then switch between the two views with `Show Store` and `Show List`.
### Before Start
A welcome page is shown after each restart. Agree to continue, or close the plugin if you prefer not to use it.
### Show Store
The store view fetches available configurations from GitHub.
Use `refresh` to reload the list if nothing appears.
Do not spam `refresh`; GitHub rate limits unauthenticated requests.
Each card lets you `install` a configuration.
An information panel is also available to explain the store view.
| Action | Effect |
| --- | --- |
| Refresh | Refresh the GitHub API data |
| Info | Show information about the store |
| Install | Install the configuration in `~/Config-Swap` |
| Show list | Switch to the list view |
### Show List
The list view shows configurations already installed in `~/Config-Swap`.
From there, you can `apply` them again or `delete` them again to refresh the files.
You will be asked to confirm before applying the selected configuration and confirm before deleting the selected configuration.
Click a preview image to see extra information such as the author, origin, and description.
Use `Show Store` to return to the GitHub store view.
| Action | Effect |
| --- | --- |
| Refresh | Refresh the GitHub API data |
| Delete | Delete the configuration in `~/Config-Swap` |
| Apply | Apply the configuration |
| Setting | Open the Config Swap settings |
| Click the preview | Open extra information |
| Show store | Switch to the store view |
### Setting Info
A settings panel is available. At this time, only saving a configuration is supported.
You can save your current configuration under a custom name for backup or export.
Saving a configuration copies the current `settings.toml` and creates an `info.json` file, which is important for the plugin and for export to the GitHub store. The `info.json` file can be edited to add information, and you can also add a `preview.png` and a default wallpaper as `wallpaper.png`.
Files are saved in `~/Config-Swap/{name}`.
This also refreshes the list of installed configurations.
| Action | Effect |
| --- | --- |
| Input field | Enter a custom name for the save (default: `save`) |
| Save | Create a backup of the current configuration |
### Extra Info
You can add your own configuration folder to `~/Config-Swap`.
Each configuration should follow the same structure as the downloaded ones, including an `info.json` file and the expected asset files.
If you want to publish a configuration, add it to the `Config-Swap-Box` repository: https://github.com/Tadomika-Ari/Config-Swap-Box
Make sure Config Swap is enabled in your plugin list before you try to use it.
You can delete a configuration with the trash button. A warning panel will appear before deletion.
### Contributing
You can contribute to Config Swap! Add your own configuration files and wallpaper to the store so others can use them.
To do so:
* Go to the Settings section and save your configuration. The plugin will copy your `settings.toml` and create an `info.json` file.
* Take a preview image and wallpaper, then go to `~/Config-Swap/{name}` and copy your `preview.png` and `wallpaper.png` files (the exact names are required).
* Update your `info.json` with important information such as the author and description.
* Go to the GitHub page linked from the information panel in the Store section and create a pull request.
* Wait for review, and then your configuration can be shared.
## Settings
No setting needed
## Requirements
- Noctalia ≥ 5.0.0
- `cp`
- `rm`
- internet access for the store view
## Install
Install the plugin and add it to your bar.
## License
MIT.
+655
View File
@@ -0,0 +1,655 @@
--!nonstrict
-- Config Swap panel
-- Local variables
local configBox = "~/Config-Swap"
local listConfig = nil
local pickSlots = {}
local columns = 2
local previewWidth = 470
local previewHeight = 350
local previewImage = ""
local selectedView = 0 -- Controls which screen to render (0=welcome, 1=installed, 2=store)
local previewCache = {} -- In-memory map: config name -> cached preview image path
local previewPending = {}
local configWidth = 800 -- Preview dialog size
local configHeight = 600
-- Data model
type infoConfig = {
name: string, -- Configuration name
author: string, -- Configuration author
origin: string, -- Distribution/source (NixOS, Arch, CachyOS, etc.)
preview: string, -- Preview image path (preview.png)
path: string, -- Config path (example: config/{name})
description: string, -- Short description
wallpaperPath: string, -- Wallpaper path
depedencie: boolean, -- Whether extra dependencies are required
}
-- Resolve Noctalia state directory
local function resolveStateDir()
return noctalia.getenv("NOCTALIA_STATE_HOME")
or ((noctalia.getenv("XDG_STATE_HOME") or ((noctalia.getenv("HOME") or "") .. "/.local/state")) .. "/noctalia")
end
-- Detecte dangerous charactère
function isDetect(target: string)
local match_string = "[/.$();]"
if string.match(target, match_string) then
return true
else
return false
end
end
-- Store listing cache
local allConfig: { infoConfig } = {}
local requestFetchList = {
url = "https://api.github.com/repos/Tadomika-Ari/Config-Swap-Box/contents/config",
method = "GET",
headers = { "Accept: application/json", "User-Agent: Config-Swap-Box-Plugin" },
follow_redirects = true
}
-- Translation helper
function tr(key, subst)
if subst then
return noctalia.tr("panel." .. key, subst)
end
return noctalia.tr("panel." .. key)
end
-- Build settings.toml path from the resolved state dir
function takePosSetting()
local noctaliaStateDir = resolveStateDir()
local settingsTomlPath = noctaliaStateDir .. "/settings.toml"
return settingsTomlPath
end
-- Shell escaping to safely build external commands
local function shellQuote(s)
return "'" .. tostring(s):gsub("'", "'\\''") .. "'"
end
-- Welcome warning screen
local function welcomeWarning() -- Shown after startup; refusing closes the plugin
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
ui.label({ text = tr("title"), textAlign = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
}),
ui.label({ text = tr("welcome_message_title"), textAlign = "center", fontSize = 16, fontWeight = "bold", color = "on_surface" }),
ui.label({ text = tr("welcome_message1"), textAlign = "center", fontSize = 14, color = "on_surface_variant" }),
ui.label({ text = tr("welcome_message2"), textAlign = "center", fontSize = 14, color = "on_surface_variant" }),
ui.row({ gap = 8, justify = "center" }, {
ui.button({ text = tr("not_agree"), variant = "ghost", onClick = "onCloseClicked" }),
ui.button({ text = tr("agree"), onClick = function()
selectedView = 1
render()
end
}),
}),
}))
end
local function renderConfirmApply(data: infoConfig) -- Confirmation dialog before applying a config
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
ui.label({ text = tr("title"), textAlign = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
}),
ui.label({ text = tr("confirmation"), textAlign = "center", fontSize = 16, fontWeight = "bold", color = "on_surface" }),
ui.label({ text = data.name, textAlign = "center", fontSize = 14, color = "on_surface_variant" }),
ui.label({ text = tr("description_confirmation"), textAlign = "center", fontSize = 13, color = "on_surface_variant" }),
ui.row({ gap = 8, justify = "center" }, {
ui.button({ text = tr("cancel"), variant = "ghost", onClick = "render" }),
ui.button({ text = tr("apply"), variant = "primary", onClick = function()
applyConfig(data)
render()
end
}),
}),
}))
end
-- Apply selected config and wallpaper
function applyConfig(data: infoConfig)
noctalia.notify(tr("apply_notification_title"))
if isDetect(data.name) == true then -- detecte malicious name
noctalia.notify(tr("apply_notification_title"), "Invalid name. Please check your info.json")
return
end
local pathSettings = takePosSetting()
if not pathSettings then
noctalia.notify(tr("notification_title"), "Path not found, please check your setting toml")
return
end
local src = shellQuote(noctalia.expandPath(configBox .. "/" .. data.name .. "/settings.toml"))
local dest = shellQuote(noctalia.expandPath(pathSettings))
local cmd = "cp " .. src .. " " .. dest
noctalia.runAsync(cmd, function(result)
if result and result.exitCode == 0 then
noctalia.setWallpaper(configBox .. "/" .. data.name .. "/wallpaper.png")
else
noctalia.notify(tr("notification_title"), "swap failed")
end
end)
end
-- Download the full config bundle: settings, info json, preview, wallpaper
function downloadConfig(data: infoConfig)
noctalia.notify(tr("install_notification_title"))
if isDetect(data.name) == true then
noctalia.notify("Download", "Invalide name detected, please check target config")
return
end
noctalia.mkdirAll(configBox .. "/" .. data.name)
local path = data.path .. "/settings.toml"
local url = "https://raw.githubusercontent.com/Tadomika-Ari/Config-Swap-Box/main/" .. path
noctalia.download(url, configBox .. "/" .. data.name .. "/settings.toml" , function(ok)
if ok then
noctalia.notify(tr("notification_title"), tr("download_ok"))
else
noctalia.notify(tr("notification_title"), tr("download_failed"))
end
end)
local path = data.path .. "/info.json"
local url = "https://raw.githubusercontent.com/Tadomika-Ari/Config-Swap-Box/main/" .. path
noctalia.download(url, configBox .. "/" .. data.name .. "/info.json" , function(ok)
if ok then
noctalia.notify(tr("notification_title"), tr("download_ok"))
else
noctalia.notify(tr("notification_title"), tr("download_failed"))
end
end)
local path = data.path .. "/preview.png"
local url = "https://raw.githubusercontent.com/Tadomika-Ari/Config-Swap-Box/main/" .. path
noctalia.download(url, configBox .. "/" .. data.name .. "/preview.png" , function(ok)
if ok then
noctalia.notify(tr("notification_title"), tr("download_ok"))
else
noctalia.notify(tr("notification_title"), tr("download_failed"))
end
end)
local path = data.path .. "/wallpaper.png"
local url = "https://raw.githubusercontent.com/Tadomika-Ari/Config-Swap-Box/main/" .. path
noctalia.download(url, configBox .. "/" .. data.name .. "/wallpaper.png" , function(ok)
if ok then
noctalia.notify(tr("notification_title"), tr("download_ok"))
else
noctalia.notify(tr("notification_title"), tr("download_failed"))
end
end)
end
-- GitHub store preview cache
local function previewCachePath(name)
-- Keep cached previews under plugin data to avoid repeated downloads.
local dir = noctalia.pluginDataDir() .. "/preview-cache"
noctalia.mkdirAll(dir)
return dir .. "/" .. name .. ".png"
end
local function queuePreviewDownload(name)
-- Prevent duplicate requests for the same preview while one is in flight.
if previewCache[name] ~= nil or previewPending[name] then
return
end
local dest = previewCachePath(name)
if noctalia.fileExists(dest) then
previewCache[name] = dest
return
end
previewPending[name] = true
local url = "https://raw.githubusercontent.com/Tadomika-Ari/Config-Swap-Box/main/config/" .. name .. "/preview.png"
noctalia.download(url, dest, function(ok)
previewPending[name] = nil
if ok then
previewCache[name] = dest
render()
end
end)
end
-- Delete an installed config
local function delete(name)
if not name or name == "" then
noctalia.notify("Delete", "invalid name")
return
end
if isDetect(name) == true then -- detecte malicious name
noctalia.notify("Delete", "Invalide Name. Please check info.json")
return
end
local target = noctalia.expandPath(configBox .. "/" .. name)
local cmd = "rm -rf " .. shellQuote(target) -- shellQuote protects the rm target path
noctalia.runAsync(cmd, function(result)
if result and result.exitCode == 0 then
noctalia.notify("Delete Ok")
else
noctalia.notify("Delete failed")
end
end)
render()
end
local function renderDeleteConfirmation(name) -- Confirmation panel before deleting a config
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
ui.label({ text = tr("confirmation_delete") .. name .. "?", align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.button({ text = tr("delete"), onClick = function()
delete(name)
end
}),
ui.button({ glyph = "close", onClick = "render" }),
}))
end
-- Save/backup settings section
local noctaliaStateDir = resolveStateDir()
local settingsTomlPath = noctaliaStateDir .. "/settings.toml" -- Active settings file path
local saveNameInput = "save"
local function saveConfig()
local name = saveNameInput ~= "" and saveNameInput or "save"
if isDetect(name) then -- detecte special charactere
noctalia.notify("Save", "Invalid Name")
return
end
local savePath = configBox .. "/" .. name
noctalia.mkdirAll(savePath)
local settingFile: infoConfig = {
name = name,
description = "save for reset",
path = configBox .. "/" .. name,
origin = "not give",
wallpaperPath = nil,
depedencie = false,
preview = nil,
author = noctalia.getenv("USER"),
}
local jsonData = noctalia.json.encode(settingFile)
noctalia.writeFile(savePath .. "/info.json", jsonData)
local content = noctalia.readFile(settingsTomlPath)
if content then
noctalia.writeFile(savePath .. "/settings.toml", content)
noctalia.notify("Save", "Save ok : " .. name)
render()
else
noctalia.notify("Save", "settings.toml not found")
end
end
function onSaveNameChange(value)
saveNameInput = value
end
local function renderSetting()
local rowsSettings = {
ui.column({ gap = 8 }, {
ui.label({
text = tr("description_setting1"),
fontSize = 12,
color = "on_surface_variant",
}),
ui.row({ flexGrow = 1, gap = 16, align = "center" }, {
ui.input({
key = "save-name-input",
value = saveNameInput,
placeholder = "Save name",
onChange = "onSaveNameChange",
flexGrow = 1,
}),
ui.button({ text = tr("save"), onClick = function()
saveConfig()
end
}),
}),
}),
}
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
ui.label({ text = "Setting section", textAlign = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.button({ text = "X", onClick = "render"}),
ui.scroll({ gap = 12, align = "stretch", flexGrow = 1 }, rowsSettings),
}))
end
-- Store information panel
local function renderInfoStore()
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
ui.label({ text = tr("info_store_title"), textAlign = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.button({ text = tr("info_store_github_button"), onClick = function()
noctalia.copyToClipboard("https://github.com/Tadomika-Ari/Config-Swap-Box", "text/plain")
noctalia.notify(tr("notification_title"), tr("info_store_copy_link_success"))
end
}),
ui.button({ glyph = "close", onClick = "render" }),
}),
ui.scroll({ gap = 16, align = "stretch", flexGrow = 1 }, {
ui.column({ gap = 4 }, {
ui.label({ text = tr("info_store_what_is_title"), fontSize = 13, fontWeight = "bold", color = "on_surface" }),
ui.label({ text = tr("info_store_what_is_body"), fontSize = 12, color = "on_surface_variant" }),
}),
ui.column({ gap = 4 }, {
ui.label({ text = tr("info_store_installing_title"), fontSize = 13, fontWeight = "bold", color = "on_surface" }),
ui.label({ text = tr("info_store_installing_body"), fontSize = 12, color = "on_surface_variant" }),
}),
ui.column({ gap = 4 }, {
ui.label({ text = tr("info_store_applying_title"), fontSize = 13, fontWeight = "bold", color = "on_surface" }),
ui.label({ text = tr("info_store_applying_body"), fontSize = 12, color = "on_surface_variant" }),
}),
ui.column({ gap = 4 }, {
ui.label({ text = tr("info_store_community_title"), fontSize = 13, fontWeight = "bold", color = "on_surface" }),
ui.label({ text = tr("info_store_community_body"), fontSize = 12, color = "on_surface_variant" }),
}),
ui.column({ gap = 4 }, {
ui.label({ text = tr("info_how_to_submite_config_title"), fontSize = 13, fontWeight = "bold", color = "on_surface" }),
ui.label({ text = tr("info_how_to_submite_config_body"), fontSize = 12, color = "on_surface_variant" }),
}),
ui.column({ gap = 4 }, {
ui.label({ text = tr("info_store_rate_limits_title"), fontSize = 13, fontWeight = "bold", color = "on_surface" }),
ui.label({ text = tr("info_store_rate_limits_body"), fontSize = 12, color = "on_surface_variant" }),
}),
}),
}))
end
local function renderList()
pickSlots = {}
local tiles = {}
local list = listConfig or {}
for count = 1, #list, 1 do
local data = list[count]
local slotIndex = count - 1
pickSlots[slotIndex] = data.path
queuePreviewDownload(data.name)
local imagePath = previewCache[data.name] or previewImage
table.insert(tiles, ui.column({ gap = 4, key = data.path, align = "center" }, {
ui.image({
path = imagePath,
width = previewWidth,
height = previewHeight,
fit = "cover",
radius = 8,
onClick = ""
}),
ui.row( {gap = 12, align = "start"}, {
ui.button({ text = tr("install"), onClick = function()
downloadConfig(data)
end
}),
ui.label({
text = data.name,
fontSize = 11,
maxLines = 1,
maxWidth = 120,
textAlign = "center",
}),
} ),
}))
end
local rows = {}
local row = {}
for i, tile in ipairs(tiles) do
table.insert(row, tile)
if #row == columns or i == #tiles then
table.insert(rows, ui.row({ gap = 12, align = "start" }, row))
row = {}
end
end
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
ui.label({ text = tr("title"), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.label({ text = tr("store_title"), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.button({ text = tr("show_list"), onClick = function()
selectedView = 1
render()
end
}),
ui.button({ text = "Info", onClick = function()
renderInfoStore()
end
}),
ui.button({ text = tr("refresh"), onClick = "getFetchList"}),
ui.button({ glyph = "close", onClick = "onCloseClicked" }),
}),
ui.scroll({ gap = 12, align = "stretch", flexGrow = 1 }, rows),
}))
end
-- Installed config details panel
local function renderConfig(data: infoConfig)
local rows = {
ui.image({
path = configBox .. "/" .. data.name .. "/preview.png",
width = configWidth,
height = configHeight,
fit = "cover",
radius = 8,
}),
ui.label({ text = data.name, align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.label({ text = tr("created_by", { author = data.author }), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.label({ text = tr("origin", { origin = data.origin }), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.label({ text = tr("description", { description = data.description or "" }), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.button({ glyph = "trash", onClick = function()
renderDeleteConfirmation(data.name)
end
}),
ui.button({ glyph = "close", onClick = "render" }),
}
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
ui.scroll({ gap = 12, align = "stretch", flexGrow = 1 }, rows),
}))
end
-- List installed configs from ~/Config-Swap
function getListInstall()
local localPath = configBox
local list = noctalia.listDir(localPath)
local listShow: { infoConfig } = {}
for i, entryName in ipairs(list) do
local infoPath = configBox .. "/" .. entryName .. "/info.json"
local content = noctalia.readFile(infoPath)
local data = content and noctalia.json.decode(content) or nil
if not data then
-- Fallback entry when info.json is missing or invalid.
table.insert(listShow, {
name = entryName,
path = "config/" .. entryName,
author = "None",
origin = "None",
preview = nil,
description = nil,
wallpaperPath = nil,
depedencie = false,
})
continue
end
table.insert(listShow, {
name = data.name,
path = data.path,
author = data.author,
origin = data.origin,
preview = data.preview,
description = data.description,
wallpaperPath = data.wallpaperPath,
depedencie = data.depedencie,
})
end
return listShow
end
-- Render installed configs grid
local function renderInstall()
pickSlots = {}
local tiles = {}
local list = getListInstall() or {}
for count = 1, #list, 1 do
local data = list[count]
local slotIndex = count - 1
pickSlots[slotIndex] = data.path
table.insert(tiles, ui.column({ gap = 4, key = data.path, align = "center" }, {
ui.image({
path = configBox .. "/" .. data.name .. "/preview.png",
width = previewWidth,
height = previewHeight,
fit = "cover",
radius = 8,
onClick = function()
renderConfig(data)
end
}),
ui.row( {gap = 12, align = "start"}, {
ui.button({ text = "delete", onClick = function()
renderDeleteConfirmation(data.name)
end
}),
ui.label({
text = data.name,
fontSize = 11,
maxLines = 1,
maxWidth = 120,
textAlign = "center",
}),
ui.button({ text = "apply", onClick = function()
renderConfirmApply(data)
end
}),
} ),
}))
end
local rows = {}
local row = {}
for i, tile in ipairs(tiles) do
table.insert(row, tile)
if #row == columns or i == #tiles then
table.insert(rows, ui.row({ gap = 12, align = "start" }, row))
row = {}
end
end
panel.render(ui.column({ flexGrow = 1, gap = 16 }, {
ui.row({ align = "center", justify = "space_between", gap = 8 }, {
ui.label({ text = tr("title"), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.label({ text = tr("installed_title"), align = "center", fontSize = 16, fontWeight = "bold", color = "on_surface", flexGrow = 1 }),
ui.button({ text = tr("show_store"), onClick = function()
selectedView = 2
render()
end
}),
ui.button({ text = "Setting", onClick = function()
renderSetting()
end
}),
ui.button({ text = tr("refresh"), onClick = "getFetchList"}),
ui.button({ glyph = "close", onClick = "onCloseClicked" }),
}),
ui.scroll({ gap = 12, align = "stretch", flexGrow = 1 }, rows),
}))
end
-- Root view router
function render()
if selectedView == 0 then
welcomeWarning()
end
if selectedView == 1 then
renderInstall()
end
if selectedView == 2 then
renderList()
end
end
-- Fetch available configs from GitHub
function getFetchList(onDone)
allConfig = {}
noctalia.http(requestFetchList, function(respond)
noctalia.log("raw body: " .. tostring(respond.body))
if not respond.ok then
noctalia.notify("Error", respond.status)
end
local data, err = noctalia.json.decode(respond.body)
if err then
noctalia.log(err)
return
end
for i, item in ipairs(data) do
table.insert(allConfig, {
name = item.name,
path = item.path,
author = nil,
depedencie = nil,
description = nil,
origin = nil,
preview = nil,
wallpaperPath = nil,
})
end
listConfig = allConfig
render()
end)
end
-- Plugin entry points
function onOpen(_context)
render()
end
function onCloseClicked()
panel.close()
end
+1
View File
@@ -0,0 +1 @@
noctalia.mkdirAll("~/Config-Swap")
+7
View File
@@ -0,0 +1,7 @@
function update()
barWidget.setGlyph("file")
end
function onClick()
noctalia.togglePanel("tadomika_ari/config-swap:panel")
end
+24
View File
@@ -0,0 +1,24 @@
id = "tadomika_ari/config-swap"
name = "Config Swap"
version = "1.0.0"
plugin_api = 4
author = "TadomiKa-Ari"
license = "MIT"
dependencies = ["cp", "rm"]
icon = "file"
description = "A Noctalia widget for applying Configuration file with one click"
tags = ["utility"]
[[widget]]
id = "widget"
entry = "config-swap-widget.luau"
[[panel]]
id = "panel"
entry = "config-swap-panel.luau"
width = 1000
height = 800
[[service]]
id = "start"
entry = "config-swap-service.luau"
Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

+47
View File
@@ -0,0 +1,47 @@
{
"panel": {
"apply": "Apply",
"apply_notification_title": "Apply",
"created_by": "Created by {author}",
"description": "Description: {description}",
"download_failed": "Download failed",
"download_ok": "Download complete",
"install": "Install",
"install_notification_title": "Install",
"installed_title": "Installed Configs",
"notification_title": "Config Swap",
"origin": "Origin: {origin}",
"refresh": "Refresh",
"show_list": "Show List",
"show_store": "Show Store",
"store_title": "Config Store",
"title": "Config Swap",
"welcome_message_title": "Welcome to Config Swap!",
"welcome_message1": "Please read this carefully before you continue.",
"welcome_message2": "This plugin can replace your current Noctalia configuration. We recommend backing up your current settings first — use the Save button in the Installed section. Backups are stored in ~/Config-Swap.",
"not_agree": "Not now",
"agree": "I understand",
"confirmation": "Apply this configuration?",
"description_confirmation": "This will replace your current settings.toml and wallpaper. A backup is not created automatically unless you saved one first.",
"cancel": "Cancel",
"confirmation_delete": "Are you sure too delete ",
"delete": "delete",
"description_setting1": "Save your current configuration under a custom name at ~/Config-Swap/{name}. A default info.json is created and can be edited later at ~/Config-Swap/{name}/info.json. To set a preview, you can add preview.png to the config folder. This also works for wallpapers using wallpaper.png",
"save": "save",
"info_store_title": "Info Store section",
"info_store_github_button": "github",
"info_store_copy_link_success": "copy link success",
"info_store_what_is_title": "What is the Store?",
"info_store_what_is_body": "The Store lists community-submitted configuration profiles hosted on the Config-Swap-Box GitHub repository. Each profile bundles a settings.toml, a preview image, and an optional wallpaper.",
"info_store_installing_title": "Installing a profile",
"info_store_installing_body": "Pressing install downloads the profile's files into ~/Config-Swap/{name}. This does not change your current configuration yet.",
"info_store_applying_title": "Applying a profile",
"info_store_applying_body": "Pressing apply replaces your active settings.toml and wallpaper with the selected profile. This is not reversible unless you saved a backup first from the Setting section.",
"info_store_community_title": "Community content",
"info_store_community_body": "Profiles are submitted by community members and are reviewed by the owner.",
"info_store_rate_limits_title": "Rate limits",
"info_store_rate_limits_body": "The store list is fetched from the GitHub API, which limits unauthenticated requests. Avoid refreshing repeatedly in a short period of time.",
"info_how_to_submite_config_title": "How to submit a configuration",
"info_how_to_submite_config_body": "You can contribute to Config Swap by adding your own configuration files and wallpaper to the store so others can use them.\n\nTo do so:\n* Go to the Settings section and save your configuration. The plugin will copy your settings.toml and create an info.json file.\n* Take a preview image and wallpaper, then go to ~/Config-Swap/{name} and copy your preview.png and wallpaper.png files. The exact names are required.\n* Update your info.json with important information such as the author and description.\n* Go to the GitHub page linked from the information panel in the Store section and create a pull request.\n* Wait for review, and then your configuration can be shared."
}
}