Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

---

## [v0.1.0] - 2025-10-01

#### Changed

- Commands with missing binary dependencies (e.g., `qmlformat`, `curl`) are now gracefully disabled and marked with an `(disabled)` tag in the help text.
- Running a disabled command now prints a clear, user-friendly message explaining which dependency is missing.
- The previous behavior of auto-installing dependencies has been removed to avoid crashes in different distributions. Similar to [this issue](https://github.com/PRASSamin/prasmoid/issues/16)
- Added a new `prasmoid fix` command that runs a script to install all required dependencies for your distribution.
- The `setup` command has been dropped in favor of the `fix` command.
- The update-checking mechanism has been completely rewritten to use SHA256 checksums for verification instead of version tags, ensuring notifications are always accurate.
- The CLI's help message has been reorganized with a new "Maintenance Commands" group for better readability.
- The main `install` and `update` shell scripts have been rewritten to be more robust, POSIX-compliant, and no longer depend on `jq`.

## [v0.0.4] - 2025-09-12

#### Changed
Expand Down Expand Up @@ -58,4 +71,3 @@
## [v0.0.1] - 2025-07-28

- **Initial Release**: Initial release of the project

55 changes: 47 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,54 @@ While the core structure of KDE Plasma plasmoids is straightforward, the surroun

One of its most revolutionary features is a **built-in, zero-dependency JavaScript runtime**. This allows you to extend the CLI with your own custom commands, automating any workflow imaginable, directly within your project – no Node.js installation required!

## Getting Started
## 🚀 Getting Started

### Installation

Prasmoid is designed for quick and easy installation. Choose your preferred method:
Prasmoid is designed for quick and easy installation. You can use the provided installer script or install dependencies manually.

> [!IMPORTANT]
> The installer script requires jq to be installed for parsing GitHub API responses.
> Install it via `sudo apt install jq`, `sudo dnf install jq`, `sudo pacman -S jq` depending on your distro.
---

### 🛠 Dependencies

Prasmoid depends on the following tools being available on your system:

- **plasmoidviewer** – for testing and running plasmoids
- **qmlformat** – for formatting QML files
- **curl** – for fetching release assets
- **gettext** – for translation tools (`xgettext`, `msgmerge`, etc.)

> [!NOTE]
> Package names may differ depending on your Linux distribution.
> On some systems, these tools are bundled in development kits such as **Plasma SDK** or **Qt tools**.
> If the installer cannot resolve them automatically, please install the tools manually using your package manager.

> [!IMPORTANT]
> Want to extend installer support for your distro?
> Please [open an issue](https://github.com/PRASSamin/prasmoid/issues) or submit a PR.
> We just need a regular user of your distro to help test and add support.

---

### 📦 Manual Dependency Installation (per distro)

If you prefer to install dependencies manually (instead of using the installer), here are the commands for supported package managers:

```bash
# Debian/Ubuntu
sudo apt install -y curl qt6-tools-dev plasma-sdk gettext

# Fedora
sudo dnf install -y curl qmlformat plasma-sdk gettext

# Arch Linux
sudo pacman -Sy --noconfirm curl qt6-declarative plasma-sdk gettext
sudo ln -s /usr/lib/qt6/bin/qmlformat /usr/bin/qmlformat

# Alpine
sudo apk add curl qt6-qttools-dev plasma-sdk gettext
sudo ln -s /usr/lib/qt6/bin/qmlformat /usr/bin/qmlformat
```

#### Recommended: Standard Build (Native)

Expand Down Expand Up @@ -203,7 +242,6 @@ Prasmoid provides a comprehensive set of commands to manage your plasmoid projec

| Command | Description | Usage & Flags |
| :------------------ | :---------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------- |
| `setup` | Bootstraps the development environment (e.g. installs dependencies). | `prasmoid setup` |
| `init` | Initializes a new plasmoid project. | `prasmoid init [-n <name>]` <br> `-n, --name`: Project name. |
| `build` | Packages the project into a `.plasmoid` archive. | `prasmoid build [-o <output_dir>]` <br> `-o, --output`: Output directory (default: `./build`). |
| `preview` | Launches the plasmoid in a live preview window. | `prasmoid preview [-w]` <br> `-w, --watch`: Auto-restart on file changes. |
Expand All @@ -216,8 +254,8 @@ Prasmoid provides a comprehensive set of commands to manage your plasmoid projec
| `changeset add` | Creates a new changeset with version bump and summary. | `prasmoid changeset add [-b <type>] [-s <summary>]` <br> `-b, --bump`: `patch`, `minor`, or `major`. <br> `-s, --summary`: Changelog summary. |
| `changeset apply` | Applies pending changesets to `metadata.json` and `CHANGELOG.md`. | `prasmoid changeset apply` |
| `command` | Manages custom JavaScript CLI commands. | See subcommands below. |
| `command add` | Adds a new custom JS command in `.prasmoid/commands/`. | `prasmoid command add [-n <name>]` <br> `-n, --name`: Command name. |
| `command remove` | Removes a custom command. | `prasmoid command remove [-n <name>]` <br> `-n, --name`: Command name. |
| `command add` | Adds a new custom JS command in `.prasmoid/commands/`. | `prasmoid command add [-n <name>]` <br> `-n, --name`: Command name. |
| `command remove` | Removes a custom command. | `prasmoid command remove [-n <name>]` <br> `-n, --name`: Command name. |
| `i18n` | Handles internationalization tasks. | See subcommands below. |
| `i18n extract` | Extracts strings for translation from metadata and QML files. | `prasmoid i18n extract` <br> `--no-po`: Skip `.po` generation. |
| `i18n compile` | Compiles `.po` files into `.mo` files for use in plasmoids. | `prasmoid i18n compile` <br> `-s, --silent`: Suppress output. |
Expand All @@ -227,6 +265,7 @@ Prasmoid provides a comprehensive set of commands to manage your plasmoid projec
| `regen types` | Regenerates `prasmoid.d.ts`. | `prasmoid regen types` |
| `regen config` | Regenerates `prasmoid.config.js`. | `prasmoid regen config` |
| `upgrade` | Updates Prasmoid itself to the latest version. | `prasmoid upgrade` |
| `fix` | Install missing dependencies or fix other issues. | `prasmoid fix` |

## Extending Prasmoid with Custom Commands

Expand Down
52 changes: 52 additions & 0 deletions cmd/fix/fix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
Copyright 2025 PRAS

This command implements the Prasmoid CLI fix functionality using a remote fix script. Instead of embedding complex fix logic directly in the Go code, which would increase the binary size by approximately 2MB, this approach leverages a lightweight shell script hosted on GitHub. This design choice ensures that Prasmoid remains lightweight while maintaining robust fix capabilities.
*/
package fix

import (
"fmt"
"os"

"github.com/fatih/color"
"github.com/spf13/cobra"

root "github.com/PRASSamin/prasmoid/cmd"
)

func init() {
if utilsIsPackageInstalled("curl") {
cliFixCmd.Short = "Install missing dependencies."
} else {
cliFixCmd.Short = fmt.Sprintf("Install missing dependencies %s", color.RedString("(disabled)"))
}
cliFixCmd.GroupID = "cli"
root.RootCmd.AddCommand(cliFixCmd)
}

var cliFixCmd = &cobra.Command{
Use: "fix",
Run: func(cmd *cobra.Command, args []string) {
if !utilsIsPackageInstalled("curl") {
fmt.Println(color.YellowString("fix command is disabled due to missing curl dependency."))
fmt.Println(color.BlueString("Please install curl and try again."))
return
}

if err := utilsCheckRoot(); err != nil {
fmt.Println(color.RedString(err.Error()))
return
}

cmdStr := fmt.Sprintf("sudo curl -sSL %s | bash", scriptURL)

command := execCommand("bash", "-c", cmdStr)
command.Stdout = os.Stdout
command.Stderr = os.Stderr

if err := command.Run(); err != nil {
fmt.Println(color.RedString("Fix failed: %v", err))
}
},
}
106 changes: 106 additions & 0 deletions cmd/fix/fix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package fix

import (
"bytes"
"errors"
"io"
"os"
"os/exec"
"testing"

"github.com/fatih/color"
"github.com/stretchr/testify/assert"
)

func TestCliFixCmd(t *testing.T) {
// Save original functions
originalUtilsIsPackageInstalled := utilsIsPackageInstalled
originalCheckRoot := utilsCheckRoot
originalExecCommand := execCommand

t.Cleanup(func() {
utilsIsPackageInstalled = originalUtilsIsPackageInstalled
utilsCheckRoot = originalCheckRoot
execCommand = originalExecCommand
})

// Helper to capture stdout
captureOutput := func() (*bytes.Buffer, func()) {
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
color.Output = w
buf := new(bytes.Buffer)
return buf, func() {
_ = w.Close()
_, _ = io.Copy(buf, r)
os.Stdout = oldStdout
color.Output = oldStdout
}
}

t.Run("curl not installed", func(t *testing.T) {
// Arrange
utilsIsPackageInstalled = func(pkg string) bool { return false }
buf, restore := captureOutput()

// Act
cliFixCmd.Run(cliFixCmd, []string{})

// Assert
restore()
output := buf.String()
assert.Contains(t, output, "fix command is disabled due to missing curl dependency.")
})

t.Run("checkRoot fails", func(t *testing.T) {
// Arrange
utilsIsPackageInstalled = func(pkg string) bool { return true }
utilsCheckRoot = func() error { return errors.New("not root") }
buf, restore := captureOutput()

// Act
cliFixCmd.Run(cliFixCmd, []string{})

// Assert
restore()
output := buf.String()
assert.Contains(t, output, "not root")
})

t.Run("exec command fails", func(t *testing.T) {
// Arrange
utilsIsPackageInstalled = func(pkg string) bool { return true }
utilsCheckRoot = func() error { return nil }
execCommand = func(name string, arg ...string) *exec.Cmd {
return exec.Command("bash", "-c", "exit 1")
}
buf, restore := captureOutput()

// Act
cliFixCmd.Run(cliFixCmd, []string{})

// Assert
restore()
output := buf.String()
assert.Contains(t, output, "Fix failed")
})

t.Run("exec command succeeds", func(t *testing.T) {
// Arrange
utilsIsPackageInstalled = func(pkg string) bool { return true }
utilsCheckRoot = func() error { return nil }
execCommand = func(name string, arg ...string) *exec.Cmd {
return exec.Command("true")
}
buf, restore := captureOutput()

// Act
cliFixCmd.Run(cliFixCmd, []string{})

// Assert
restore()
output := buf.String()
assert.NotContains(t, output, "Fix failed")
})
}
16 changes: 16 additions & 0 deletions cmd/fix/vars.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package fix

import (
"os/exec"

"github.com/PRASSamin/prasmoid/utils"
)

var (
execCommand = exec.Command

utilsIsPackageInstalled = utils.IsPackageInstalled
utilsCheckRoot = utils.CheckRoot

scriptURL = "https://raw.githubusercontent.com/PRASSamin/prasmoid/main/scripts/fix"
)
43 changes: 14 additions & 29 deletions cmd/format/format.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ import (
"sync"
"time"

"github.com/AlecAivazis/survey/v2"
"github.com/PRASSamin/prasmoid/cmd"
"github.com/PRASSamin/prasmoid/consts"
"github.com/PRASSamin/prasmoid/utils"
"github.com/fatih/color"
"github.com/fsnotify/fsnotify"
Expand All @@ -41,12 +39,9 @@ func (w *watcherWrapper) Errors() chan error {
}

var (
utilsIsPackageInstalled = utils.IsPackageInstalled
utilsIsValidPlasmoid = utils.IsValidPlasmoid
utilsDetectPackageManager = utils.DetectPackageManager
surveyAskOne = survey.AskOne
utilsIsQmlFile = utils.IsQmlFile
utilsInstallPackage = utils.InstallPackage
utilsIsPackageInstalled = utils.IsPackageInstalled
execCommand = exec.Command
// for testing
filepathWalk = filepath.Walk
Expand All @@ -66,40 +61,30 @@ var dir string
func init() {
FormatCmd.Flags().BoolVarP(&watch, "watch", "w", false, "watch for changes")
FormatCmd.Flags().StringVarP(&dir, "dir", "d", "./contents", "directory to format")

if utilsIsPackageInstalled("qmlformat") {
FormatCmd.Short = "Prettify QML files"
} else {
FormatCmd.Short = fmt.Sprintf("Prettify QML files %s", color.RedString("(disabled)"))
}

cmd.RootCmd.AddCommand(FormatCmd)
}

// FormatCmd represents the format command
var FormatCmd = &cobra.Command{
Use: "format",
Short: "Prettify QML files",
Long: "Automatically format QML source files to ensure consistent style and readability.",
Run: func(cmd *cobra.Command, args []string) {
if !utilsIsValidPlasmoid() {
fmt.Println(color.RedString("Current directory is not a valid plasmoid."))
if !utilsIsPackageInstalled("qmlformat") {
fmt.Println(color.YellowString("format command is disabled due to missing qmlformat dependency."))
fmt.Println(color.BlueString("- Use `prasmoid fix` to install it."))
return
}
if !utilsIsPackageInstalled(consts.QmlFormatPackageName["binary"]) {
pm, _ := utilsDetectPackageManager()
var confirm bool
confirmPrompt := &survey.Confirm{
Message: "qmlformat is not installed. Do you want to install it?",
Default: true,
}
if err := surveyAskOne(confirmPrompt, &confirm); err != nil {
return
}

if confirm {
if err := utilsInstallPackage(pm, consts.QmlFormatPackageName["binary"], consts.QmlFormatPackageName); err != nil {
fmt.Println(color.RedString("Failed to install qmlformat."))
return
}
fmt.Println(color.GreenString("qmlformat installed successfully."))
} else {
fmt.Println(color.YellowString("Operation cancelled."))
return
}
if !utilsIsValidPlasmoid() {
fmt.Println(color.RedString("Current directory is not a valid plasmoid."))
return
}

crrPath, _ := os.Getwd()
Expand Down
Loading