summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/.gitignore2
-rw-r--r--src/go.mod5
-rw-r--r--src/main.go411
3 files changed, 418 insertions, 0 deletions
diff --git a/src/.gitignore b/src/.gitignore
new file mode 100644
index 0000000..973bad1
--- /dev/null
+++ b/src/.gitignore
@@ -0,0 +1,2 @@
1kjagave
2go.sum \ No newline at end of file
diff --git a/src/go.mod b/src/go.mod
new file mode 100644
index 0000000..08ebd30
--- /dev/null
+++ b/src/go.mod
@@ -0,0 +1,5 @@
1module kjagave
2
3go 1.21
4
5require github.com/gotk3/gotk3 v0.6.3
diff --git a/src/main.go b/src/main.go
new file mode 100644
index 0000000..d734583
--- /dev/null
+++ b/src/main.go
@@ -0,0 +1,411 @@
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "log"
7 "os"
8 "os/exec"
9 "path/filepath"
10 "strings"
11
12 "github.com/gotk3/gotk3/gdk"
13 "github.com/gotk3/gotk3/glib"
14 "github.com/gotk3/gotk3/gtk"
15)
16
17const (
18 appTitle = "kjagave"
19 appVersion = "1.0"
20)
21
22type SavedColor struct {
23 Hex string `json:"hex"`
24 Name string `json:"name"`
25}
26
27type App struct {
28 window *gtk.Window
29 colorButton *gtk.ColorButton
30 currentColor *gdk.RGBA
31 listStore *gtk.ListStore
32 treeView *gtk.TreeView
33 deleteBtn *gtk.Button
34 savedColors []SavedColor
35 configFile string
36 selectedIter *gtk.TreeIter
37}
38
39func main() {
40 gtk.Init(nil)
41
42 configDir := filepath.Join(os.Getenv("HOME"), ".config")
43 os.MkdirAll(configDir, 0755)
44
45 app := &App{
46 configFile: filepath.Join(configDir, "kjagave.json"),
47 }
48
49 app.loadColors()
50 app.createUI()
51 app.populateList()
52
53 gtk.Main()
54}
55
56func (app *App) createUI() {
57 var err error
58
59 // main window
60 app.window, err = gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
61 if err != nil {
62 log.Fatal("Unable to create window:", err)
63 }
64 app.window.SetTitle(appTitle)
65 app.window.SetDefaultSize(550, 450)
66 app.window.SetResizable(false)
67 app.window.Connect("destroy", gtk.MainQuit)
68
69 // vertical box
70 mainBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 10)
71 mainBox.SetMarginTop(15)
72 mainBox.SetMarginBottom(15)
73 mainBox.SetMarginStart(15)
74 mainBox.SetMarginEnd(15)
75
76 // color selection area
77 colorBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 10)
78 colorBox.SetHAlign(gtk.ALIGN_CENTER)
79
80 label, _ := gtk.LabelNew("Select Color:")
81 colorBox.PackStart(label, false, false, 0)
82
83 // initialize color
84 app.currentColor = gdk.NewRGBA()
85 app.currentColor.Parse("#69BAA7")
86
87 app.colorButton, err = gtk.ColorButtonNewWithRGBA(app.currentColor)
88 if err != nil {
89 log.Fatal("Unable to create color button:", err)
90 }
91 app.colorButton.SetUseAlpha(true)
92 app.colorButton.SetTitle("Choose a Color")
93 app.colorButton.Connect("color-set", func() {
94 app.currentColor = app.colorButton.GetRGBA()
95 })
96
97 colorBox.PackStart(app.colorButton, false, false, 0)
98
99 hexEntry, _ := gtk.EntryNew()
100 hexEntry.SetEditable(false)
101 hexEntry.SetWidthChars(10)
102 hexEntry.SetText(rgbaToHex(app.currentColor))
103 colorBox.PackStart(hexEntry, false, false, 0)
104
105 // color picker button
106 pickerBtn, _ := gtk.ButtonNewWithLabel("Pick from Screen")
107 pickerBtn.Connect("clicked", func() {
108 if color, err := app.pickColorFromScreen(); err == nil {
109 app.colorButton.SetRGBA(color)
110 app.currentColor = color
111 hexEntry.SetText(rgbaToHex(color))
112 }
113 })
114 colorBox.PackStart(pickerBtn, false, false, 0)
115
116 // bump hex entry when color changes
117 app.colorButton.Connect("color-set", func() {
118 app.currentColor = app.colorButton.GetRGBA()
119 hexEntry.SetText(rgbaToHex(app.currentColor))
120 })
121
122 mainBox.PackStart(colorBox, false, false, 0)
123
124 expander, _ := gtk.ExpanderNew("Saved Colors")
125 expander.SetExpanded(true)
126 expanderBox, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 5)
127 expanderBox.SetMarginTop(5)
128 expanderBox.SetMarginBottom(5)
129 btnBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
130 btnBox.SetHAlign(gtk.ALIGN_END)
131 app.deleteBtn, _ = gtk.ButtonNewWithLabel("Delete")
132 app.deleteBtn.SetSensitive(false)
133 app.deleteBtn.Connect("clicked", app.onDeleteClicked)
134 btnBox.PackStart(app.deleteBtn, false, false, 0)
135 saveBtn, _ := gtk.ButtonNewWithLabel("Save...")
136 saveBtn.Connect("clicked", app.onSaveClicked)
137 btnBox.PackStart(saveBtn, false, false, 0)
138 expanderBox.PackStart(btnBox, false, false, 0)
139
140 // treeview
141 scrolled, _ := gtk.ScrolledWindowNew(nil, nil)
142 scrolled.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
143 scrolled.SetSizeRequest(-1, 180)
144
145 app.listStore, _ = gtk.ListStoreNew(gdk.PixbufGetType(), glib.TYPE_STRING, glib.TYPE_STRING)
146 app.treeView, _ = gtk.TreeViewNew()
147 app.treeView.SetModel(app.listStore)
148 app.treeView.SetHeadersVisible(true)
149
150 // color column with swatch
151 colorCol, _ := gtk.TreeViewColumnNew()
152 colorCol.SetTitle("Color")
153 colorCol.SetSortColumnID(1)
154
155 pixbufRenderer, _ := gtk.CellRendererPixbufNew()
156 colorCol.PackStart(pixbufRenderer, false)
157 colorCol.AddAttribute(pixbufRenderer, "pixbuf", 0)
158
159 textRenderer, _ := gtk.CellRendererTextNew()
160 colorCol.PackStart(textRenderer, true)
161 colorCol.AddAttribute(textRenderer, "text", 1)
162
163 app.treeView.AppendColumn(colorCol)
164
165 // name column
166 nameRenderer, _ := gtk.CellRendererTextNew()
167 nameCol, _ := gtk.TreeViewColumnNewWithAttribute("Name", nameRenderer, "text", 2)
168 nameCol.SetSortColumnID(2)
169 app.treeView.AppendColumn(nameCol)
170
171 selection, _ := app.treeView.GetSelection()
172 selection.SetMode(gtk.SELECTION_SINGLE)
173 selection.Connect("changed", app.onSelectionChanged)
174
175 scrolled.Add(app.treeView)
176 expanderBox.PackStart(scrolled, true, true, 0)
177 expander.Add(expanderBox)
178 mainBox.PackStart(expander, true, true, 0)
179
180 // bottom button box
181 bottomBox, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 5)
182 bottomBox.SetHAlign(gtk.ALIGN_END)
183
184 copyBtn, _ := gtk.ButtonNewWithLabel("Copy to Clipboard")
185 copyBtn.Connect("clicked", app.onCopyClicked)
186 bottomBox.PackStart(copyBtn, false, false, 0)
187
188 aboutBtn, _ := gtk.ButtonNewWithLabel("About")
189 aboutBtn.Connect("clicked", app.onAboutClicked)
190 bottomBox.PackStart(aboutBtn, false, false, 0)
191
192 mainBox.PackStart(bottomBox, false, false, 0)
193
194 app.window.Add(mainBox)
195 app.window.ShowAll()
196}
197
198func (app *App) populateList() {
199 app.listStore.Clear()
200
201 for _, color := range app.savedColors {
202 pixbuf := app.createColorSwatch(color.Hex)
203 iter := app.listStore.Append()
204 app.listStore.Set(iter, []int{0, 1, 2}, []interface{}{pixbuf, color.Hex, color.Name})
205 }
206}
207
208func (app *App) createColorSwatch(hexColor string) *gdk.Pixbuf {
209 pixbuf, err := gdk.PixbufNew(gdk.COLORSPACE_RGB, false, 8, 16, 14)
210 if err != nil {
211 return nil
212 }
213
214 rgba := gdk.NewRGBA()
215 rgba.Parse(hexColor)
216
217 r := uint32(rgba.GetRed() * 255)
218 g := uint32(rgba.GetGreen() * 255)
219 b := uint32(rgba.GetBlue() * 255)
220
221 pixels := pixbuf.GetPixels()
222 rowstride := pixbuf.GetRowstride()
223 nChannels := pixbuf.GetNChannels()
224
225 for y := 0; y < 14; y++ {
226 for x := 0; x < 16; x++ {
227 offset := y*rowstride + x*nChannels
228 pixels[offset] = byte(r)
229 pixels[offset+1] = byte(g)
230 pixels[offset+2] = byte(b)
231 }
232 }
233
234 return pixbuf
235}
236
237func (app *App) onSelectionChanged(selection *gtk.TreeSelection) {
238 model, iter, ok := selection.GetSelected()
239 if !ok {
240 app.deleteBtn.SetSensitive(false)
241 app.selectedIter = nil
242 return
243 }
244
245 app.selectedIter = iter
246 app.deleteBtn.SetSensitive(true)
247
248 value, _ := model.ToTreeModel().GetValue(iter, 1)
249 hexColor, _ := value.GetString()
250
251 rgba := gdk.NewRGBA()
252 rgba.Parse(hexColor)
253 app.colorButton.SetRGBA(rgba)
254 app.currentColor = rgba
255}
256
257func (app *App) onSaveClicked() {
258 dialog, _ := gtk.DialogNew()
259 dialog.SetTitle("Save Color")
260 dialog.SetTransientFor(app.window)
261 dialog.SetModal(true)
262 dialog.SetDefaultSize(300, -1)
263
264 box, _ := dialog.GetContentArea()
265 box.SetSpacing(10)
266 box.SetMarginTop(10)
267 box.SetMarginBottom(10)
268 box.SetMarginStart(10)
269 box.SetMarginEnd(10)
270
271 // get current color
272 hexColor := rgbaToHex(app.currentColor)
273
274 label, _ := gtk.LabelNew(fmt.Sprintf("Color: %s", hexColor))
275 box.PackStart(label, false, false, 0)
276
277 entryLabel, _ := gtk.LabelNew("Color Name:")
278 entryLabel.SetHAlign(gtk.ALIGN_START)
279 box.PackStart(entryLabel, false, false, 0)
280
281 entry, _ := gtk.EntryNew()
282 entry.SetText("Untitled")
283 entry.SetActivatesDefault(true)
284 box.PackStart(entry, false, false, 0)
285
286 dialog.AddButton("Cancel", gtk.RESPONSE_CANCEL)
287 okBtn, _ := dialog.AddButton("OK", gtk.RESPONSE_OK)
288 okBtn.SetCanDefault(true)
289 okBtn.GrabDefault()
290
291 dialog.ShowAll()
292
293 response := dialog.Run()
294 if response == gtk.RESPONSE_OK {
295 text, _ := entry.GetText()
296 app.savedColors = append([]SavedColor{{Hex: hexColor, Name: text}}, app.savedColors...)
297 app.saveColors()
298 app.populateList()
299 }
300
301 dialog.Destroy()
302}
303
304func (app *App) onDeleteClicked() {
305 if app.selectedIter == nil {
306 return
307 }
308
309 model := app.listStore.ToTreeModel()
310 value, _ := model.GetValue(app.selectedIter, 1)
311 hexColor, _ := value.GetString()
312
313 // remove from saved colors
314 for i, color := range app.savedColors {
315 if color.Hex == hexColor {
316 app.savedColors = append(app.savedColors[:i], app.savedColors[i+1:]...)
317 break
318 }
319 }
320
321 app.saveColors()
322 app.populateList()
323 app.deleteBtn.SetSensitive(false)
324 app.selectedIter = nil
325}
326
327func (app *App) onCopyClicked() {
328 hexColor := rgbaToHex(app.currentColor)
329
330 clipboard, _ := gtk.ClipboardGet(gdk.SELECTION_CLIPBOARD)
331 clipboard.SetText(hexColor)
332
333 dialog := gtk.MessageDialogNew(app.window, gtk.DIALOG_MODAL, gtk.MESSAGE_INFO,
334 gtk.BUTTONS_OK, fmt.Sprintf("Color %s copied to clipboard!", hexColor))
335 dialog.Run()
336 dialog.Destroy()
337}
338
339func (app *App) onAboutClicked() {
340 dialog, _ := gtk.AboutDialogNew()
341 dialog.SetTransientFor(app.window)
342 dialog.SetProgramName(appTitle)
343 dialog.SetVersion(appVersion)
344 dialog.SetComments("A color picker with screen color grabbing support")
345 dialog.SetAuthors([]string{"kjagave 2025", "Based on gcolor2 by Ned Haughton"})
346 dialog.SetLicense("GPL-2.0")
347 dialog.Run()
348 dialog.Destroy()
349}
350
351func (app *App) pickColorFromScreen() (*gdk.RGBA, error) {
352 // use xcolor for x11 color picking
353 cmd := exec.Command("xcolor", "--format", "hex")
354 output, err := cmd.Output()
355 if err != nil {
356 // fallback to grabc
357 cmd = exec.Command("grabc")
358 output, err = cmd.Output()
359 if err != nil {
360 dialog := gtk.MessageDialogNew(app.window, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR,
361 gtk.BUTTONS_OK, "Color picker not found. Please install 'xcolor' or 'grabc':\n\nsudo apt install xcolor\n# or\nsudo apt install grabc")
362 dialog.Run()
363 dialog.Destroy()
364 return nil, err
365 }
366 }
367
368 hexColor := strings.TrimSpace(string(output))
369 if !strings.HasPrefix(hexColor, "#") {
370 hexColor = "#" + hexColor
371 }
372
373 rgba := gdk.NewRGBA()
374 if !rgba.Parse(hexColor) {
375 return nil, fmt.Errorf("invalid color format: %s", hexColor)
376 }
377
378 return rgba, nil
379}
380
381func (app *App) loadColors() {
382 data, err := os.ReadFile(app.configFile)
383 if err != nil {
384 app.savedColors = []SavedColor{}
385 return
386 }
387
388 if err := json.Unmarshal(data, &app.savedColors); err != nil {
389 log.Printf("Error loading colors: %v", err)
390 app.savedColors = []SavedColor{}
391 }
392}
393
394func (app *App) saveColors() {
395 data, err := json.MarshalIndent(app.savedColors, "", " ")
396 if err != nil {
397 log.Printf("Error marshaling colors: %v", err)
398 return
399 }
400
401 if err := os.WriteFile(app.configFile, data, 0644); err != nil {
402 log.Printf("Error saving colors: %v", err)
403 }
404}
405
406func rgbaToHex(rgba *gdk.RGBA) string {
407 r := uint8(rgba.GetRed() * 255)
408 g := uint8(rgba.GetGreen() * 255)
409 b := uint8(rgba.GetBlue() * 255)
410 return fmt.Sprintf("#%02X%02X%02X", r, g, b)
411}