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
95 changes: 95 additions & 0 deletions .github/skills/solid-kiss-dry-refactor/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
---
name: solid-kiss-dry-refactor
description: "Refactor code using SOLID, KISS, and DRY principles. Use when: improving code quality, reducing duplication, simplifying complex logic, splitting responsibilities, fixing over-engineering, extracting reusable abstractions, reviewing a file or module for design smells."
argument-hint: "File or module to refactor, or describe the code smell to fix"
---

# SOLID, KISS & DRY Refactor

## When to Use
- A function or class has more than one reason to change (SRP violation)
- Logic is duplicated across two or more places (DRY violation)
- A function is hard to read or does too much at once (KISS violation)
- A class is modified every time a new variant is added (OCP violation)
- A caller depends on low-level details instead of an abstraction (DIP violation)
- Interfaces force implementors to define methods they don't use (ISP violation)

---

## Principles Reference

### SOLID
| Letter | Principle | Violation Signal |
|--------|-----------|-----------------|
| **S** | Single Responsibility — one reason to change | Class/function does fetching + parsing + rendering |
| **O** | Open/Closed — extend without modifying | Adding a new type requires editing a `switch` / `if-else` chain |
| **L** | Liskov Substitution — subtypes behave like base types | Override throws, ignores, or weakens a base-class contract |
| **I** | Interface Segregation — no forced unused methods | Interface has 10 methods but most implementors use 2 |
| **D** | Dependency Inversion — depend on abstractions | High-level module `new`s a concrete low-level class inline |

### KISS
- Prefer the simplest solution that satisfies the requirements
- Avoid unnecessary layers of indirection, abstraction, or configuration
- Code should be readable by a new contributor without explanation

### DRY
- Every piece of knowledge has one authoritative representation
- Identical logic in two places → extract to a shared function/module
- Identical structure in two places → consider a generic abstraction *only if* the duplication is genuine knowledge duplication, not coincidental similarity

---

## Procedure

### Step 1 — Analyze
1. Read the target file(s) in full.
2. List every violation found, tagged by principle (e.g. `[SRP]`, `[DRY]`, `[KISS]`).
3. Rate each violation: **High** (blocks extension / causes bugs), **Medium** (maintenance burden), **Low** (style preference).

### Step 2 — Prioritise & Plan
1. Sort violations: High → Medium → Low.
2. For each High/Medium violation, describe the refactoring move (e.g. *extract function*, *introduce interface*, *inline variable*, *replace conditional with polymorphism*).
3. If changes affect more than one module or require a new abstraction, **present the plan to the user and get approval before writing code**.

### Step 3 — Refactor (one violation at a time)
Apply refactors incrementally:
- **Extract function/method** — for KISS/SRP violations in functions > ~20 lines or doing > 1 thing.
- **Extract module/class** — for SRP violations at the class level.
- **Introduce abstraction / interface** — for OCP and DIP violations.
- **Deduplicate** — for DRY violations; create a shared utility/helper, then update all call sites.
- **Simplify** — for KISS violations; remove dead code, flatten nested logic, replace clever tricks with readable code.

After each individual refactor:
- Confirm the code still compiles / has no type errors.
- Do not change behaviour — refactoring is behaviour-preserving.

### Step 4 — Verify
1. Run the project's test suite (`npx vitest run` for this repo, or the relevant test command).
2. All tests must pass before marking done.
3. If tests break, fix them — the refactor changed a public API that tests depended on.

### Step 5 — Summarise
Report:
- What violations were found
- What was changed and why
- Any trade-offs made (e.g. skipped a Low violation to avoid over-engineering)
- Whether tests pass

---

## Decision Guardrails

| Situation | Do |
|-----------|----|
| Two functions look similar but represent *different* concepts | Leave them separate (coincidental duplication) |
| Abstraction would only be used once | Don't introduce it (YAGNI / KISS) |
| Fixing an ISP/DIP violation requires a large interface redesign | Propose it, don't do it unilaterally |
| A violation is minor and touching it risks a regression | Note it, leave it, don't fix it |

---

## Anti-Patterns to Avoid
- **Over-abstracting**: Adding factories, registries, or strategy patterns where a simple function suffices
- **Premature deduplication**: Merging two functions that happen to look the same but will diverge
- **Renaming without substance**: Improving names without fixing the structural issue
- **Splitting for splitting's sake**: Breaking a cohesive 30-line function into 5 micro-functions with no clarity gain
43 changes: 43 additions & 0 deletions electron/main.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const { app, BrowserWindow, ipcMain, dialog, protocol, net, Tray, Menu, nativeImage } = require('electron')
const path = require('path')
const fs = require('fs')
const os = require('os')
const { spawn, fork } = require('child_process')
const {
PLAYERCTL_COMMANDS,
Expand Down Expand Up @@ -442,6 +443,48 @@ function registerIpcHandlers() {
return result.canceled ? null : result.filePaths[0]
})

ipcMain.handle('apps:list', async () => {
const dirs = [
'/usr/share/applications',
'/var/lib/flatpak/exports/share/applications',
path.join(os.homedir(), '.local/share/applications'),
path.join(os.homedir(), '.local/share/flatpak/exports/share/applications'),
]
const apps = []
const seen = new Set()
for (const dir of dirs) {
let files
try { files = await fs.promises.readdir(dir) } catch { continue }
for (const file of files) {
if (!file.endsWith('.desktop')) continue
const appId = file.replace(/\.desktop$/, '')
if (seen.has(appId)) continue
seen.add(appId)
try {
const content = await fs.promises.readFile(path.join(dir, file), 'utf8')
const data = {}
let inEntry = false
for (const line of content.split('\n')) {
const t = line.trim()
if (t === '[Desktop Entry]') { inEntry = true; continue }
if (t.startsWith('[') && t !== '[Desktop Entry]') { inEntry = false; continue }
if (!inEntry) continue
const eq = t.indexOf('=')
if (eq === -1) continue
const key = t.slice(0, eq).trim()
if (key.includes('[')) continue // skip localized keys
if (!data[key]) data[key] = t.slice(eq + 1).trim()
}
if (data.Type !== 'Application') continue
if (data.NoDisplay === 'true' || data.Hidden === 'true') continue
if (!data.Name) continue
apps.push({ name: data.Name, appId })
} catch { continue }
}
}
return apps.sort((a, b) => a.name.localeCompare(b.name))
})

ipcMain.handle('profile:save', async (_, data) => {
const p = getProfilePath(activeProfileName)
await fs.promises.mkdir(path.dirname(p), { recursive: true })
Expand Down
1 change: 1 addition & 0 deletions electron/preload.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ contextBridge.exposeInMainWorld('streamDeck', {
runCommand: (command) => ipcRenderer.invoke('action:run-cmd', { command }),
sleepToggle: () => ipcRenderer.invoke('action:sleep-toggle'),
browseForFile: () => ipcRenderer.invoke('dialog:open-file'),
listApps: () => ipcRenderer.invoke('apps:list'),
browseIconDir: () => ipcRenderer.invoke('icons:browse-dir'),
scanIconDir: (dirPath) => ipcRenderer.invoke('icons:scan-dir', { dirPath }),
loadIconFile: (filePath) => ipcRenderer.invoke('icons:load-file', { filePath }),
Expand Down
80 changes: 80 additions & 0 deletions src/App.css
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,86 @@
font-style: italic;
}

/* ─── App Picker ─────────────────────────────────────────── */
.app-picker-selected {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
background: #1a2030;
border: 1px solid #2a3a55;
border-radius: 6px;
font-size: 13px;
color: #5aabff;
}

.app-picker-selected-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

.app-picker-clear-btn {
flex-shrink: 0;
background: none;
border: none;
color: #445;
cursor: pointer;
font-size: 12px;
padding: 0 0 0 8px;
line-height: 1;
transition: color 0.1s;
}

.app-picker-clear-btn:hover { color: #ff6060; }

.app-picker-dropdown {
display: flex;
flex-direction: column;
background: #161616;
border: 1px solid #2e2e2e;
border-radius: 6px;
overflow: hidden;
max-height: 220px;
overflow-y: auto;
}

.app-picker-item {
background: none;
border: none;
border-bottom: 1px solid #1e1e1e;
text-align: left;
padding: 7px 10px;
font-size: 12px;
color: #aaa;
cursor: pointer;
transition: background 0.1s, color 0.1s;
}

.app-picker-item:last-child { border-bottom: none; }
.app-picker-item:hover { background: #1e2a3a; color: #fff; }

.app-picker-no-results {
padding: 8px 10px;
font-size: 11px;
color: #444;
font-style: italic;
}

.app-picker-switch-btn {
background: none;
border: none;
text-align: left;
padding: 2px 0;
font-size: 10px;
color: #3a3a3a;
cursor: pointer;
transition: color 0.1s;
}

.app-picker-switch-btn:hover { color: #666; }

.action-hint {
font-size: 11px;
color: #3d3d3d;
Expand Down
Loading