From 8046177af0820e4a34dc55363ddd4571ca60c950 Mon Sep 17 00:00:00 2001 From: shinya Date: Wed, 3 Sep 2025 11:23:05 +0200 Subject: [PATCH] added kitty image cleanup --- docs/FS.md | 31 ++-- docs/TECH_STACK.md | 5 +- docs/TODO.md | 7 + internal/tui/chat.go | 18 ++- internal/tui/keybindings.go | 3 + internal/tui/layout.go | 1 - internal/tui/messages.go | 56 +++++++ pkg/clean_image/clean_image.go | 257 +++++++++++++++++++++++++++++++++ 8 files changed, 356 insertions(+), 22 deletions(-) create mode 100644 pkg/clean_image/clean_image.go diff --git a/docs/FS.md b/docs/FS.md index beb9736..c19515a 100644 --- a/docs/FS.md +++ b/docs/FS.md @@ -131,9 +131,10 @@ * Full support for: - * Linux - * macOS + * Linux (primary support) * Windows (via WSL or native terminal) + * macOS (should work same as on linux hopefully) + * All features should work identically or degrade gracefully. ### 2. **Performance & Responsiveness** @@ -173,18 +174,20 @@ ```shell ~/.config/whspbrd/ -├── config.json -├── keys/ -│ ├── private.asc -│ └── public.asc -├── messages/ -│ ├── user123.json -│ └── user456.json -├── media/ -│ ├── user123_avatar.png -│ └── msg_img_abc123.png -├── plugins/ -│ └── music_status.so +├──  config.json +├──  icon.png +└──  servers + ├──  default + │ ├──  server.json + │ └──  users + │ ├──  alice + │ │ ├── 󰷖 alice.pub + │ │ ├──  icon.png + │ │ └──  messages.json + │ └──  bob + │ ├── 󰷖 bob.pub + │ └──  messages.json + └──  secondary ``` --- diff --git a/docs/TECH_STACK.md b/docs/TECH_STACK.md index 71f0cf4..0cd4139 100644 --- a/docs/TECH_STACK.md +++ b/docs/TECH_STACK.md @@ -9,6 +9,9 @@ - - rendrování obrázků v terminálu NEE, napsali jsme vlastní (profilové obrázky nebo posílané médium) - - image editing NEEE, nepotřebujeme actually +- - notification messages +- - colors in terminal +- - terminal commands interface maybe instead of conda ## C @@ -31,4 +34,4 @@ - načítat a sdílet přehrávanou hudbu (discord spotify integration, but with playerctl or some other music protocol) - v go je možné ukládat do binárky standartní soubory, možná se to třeba bude [hodit](https://www.youtube.com/watch?v=7EK06n485nk&pp=ygUJZ28gZW1iZWQg) -- automaticky detekovat, že zařízení jsou na stejné síti a pak posílat komunikaci p2p \ No newline at end of file +- automaticky detekovat, že zařízení jsou na stejné síti a pak posílat komunikaci p2p diff --git a/docs/TODO.md b/docs/TODO.md index c1646d6..c3d6767 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -4,9 +4,11 @@ - [ ] Initial configuration setup - [ ] Solve colors issue +- [ ] No chat handling and no chat selected on the startup of the application - [ ] Complete chat loading and sending messages to contacts so it is written to the file - [ ] Create systray - [ ] Share what music am i listening using mpris +- [ ] Colors and themes ### Chat @@ -60,6 +62,11 @@ ## Communication +## Application integration with the system + +- [ ] Systray and how to manage to not have multiple instances of the application running +- [ ] Create simple cmd commands that would be intuitive and simple to use + ## IDEAS - just set window size of the box to be even, so there should not be need in rendering half of the profile picture diff --git a/internal/tui/chat.go b/internal/tui/chat.go index 229ec0a..41ea570 100644 --- a/internal/tui/chat.go +++ b/internal/tui/chat.go @@ -6,6 +6,7 @@ import ( "whspbrd/pkg/cell_size" "whspbrd/pkg/render_image" + "whspbrd/pkg/clean_image" "github.com/jroimartin/gocui" ) @@ -40,6 +41,11 @@ func layoutInput(g *gocui.Gui, maxX, maxY int) error { func updateChatView(v *gocui.View) { v.Clear() + + clear := cleanimage.NewKittyImageCleaner() + // TODO: In future optimize this to only clear certain part of screen + fmt.Print(clear.DeleteAllVisiblePlacements(true)) + for i, msg := range messages { fmt.Fprintf(v, "%s\n\n", msg) w, h, err := cell_size.GetTerminalCellSizePixels() @@ -65,11 +71,11 @@ func sendMessage(g *gocui.Gui, v *gocui.View) error { v.SetCursor(0, 0) v.SetOrigin(0, 0) - if input != "" { - messages = append(messages, "\t\t\t\t\tYou:\n\t\t\t\t\t"+input) - if chatView, err := g.View("chat"); err == nil { - updateChatView(chatView) - } - } + WriteMessage(users[selectedUserIdx], "You", users[selectedUserIdx], input) + + messages = []string{} + LoadMessages(users[selectedUserIdx]) + + updateChatView(g.Views()[1]) return nil } diff --git a/internal/tui/keybindings.go b/internal/tui/keybindings.go index 16693f9..5e469eb 100644 --- a/internal/tui/keybindings.go +++ b/internal/tui/keybindings.go @@ -17,6 +17,9 @@ func keybindings(g *gocui.Gui) error { if err := g.SetKeybinding("", gocui.KeyCtrlK, gocui.ModNone, prevContact); err != nil { return err } + if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, toggleProfileView); err != nil { + return err + } return nil } diff --git a/internal/tui/layout.go b/internal/tui/layout.go index a3ca9ff..cf36fc4 100644 --- a/internal/tui/layout.go +++ b/internal/tui/layout.go @@ -19,7 +19,6 @@ func layout(g *gocui.Gui) error { return err } if err := layoutChat(g, maxX, maxY); err != nil { - //updateChatView(g.Views()[1]) return err } if err := layoutInput(g, maxX, maxY); err != nil { diff --git a/internal/tui/messages.go b/internal/tui/messages.go index 26342a7..38f1bbc 100644 --- a/internal/tui/messages.go +++ b/internal/tui/messages.go @@ -3,9 +3,11 @@ package tui import ( "encoding/base64" "encoding/json" + "fmt" "log" "os" "path/filepath" + "strconv" "strings" "time" ) @@ -39,6 +41,60 @@ func LoadContacts(path string) { } } +func WriteMessage(username, sender, receiver, content string) error { + chatFile := filepath.Join("configs", "servers", "default", "users", strings.ToLower(username), "messages.json") + + if _, err := os.Stat(chatFile); os.IsNotExist(err) { + emptyData := ChatData{Messages: []ChatMessage{}} + data, _ := json.MarshalIndent(emptyData, "", " ") + if err := os.WriteFile(chatFile, data, 0644); err != nil { + return fmt.Errorf("failed to create chat file: %v", err) + } + } + + data, err := os.ReadFile(chatFile) + if err != nil { + return fmt.Errorf("error reading chat file: %v", err) + } + + var chatData ChatData + if len(data) > 0 { + if err := json.Unmarshal(data, &chatData); err != nil { + return fmt.Errorf("error parsing JSON: %v", err) + } + } + + newID := "1" + if len(chatData.Messages) > 0 { + lastMsg := chatData.Messages[len(chatData.Messages)-1] + if lastID, err := strconv.Atoi(lastMsg.ID); err == nil { + newID = strconv.Itoa(lastID + 1) + } + } + + encodedContent := base64.StdEncoding.EncodeToString([]byte(content)) + + newMsg := ChatMessage{ + ID: newID, + Sender: sender, + Receiver: receiver, + Content: encodedContent, + Timestamp: time.Now().UTC().Format(time.RFC3339), + } + + chatData.Messages = append(chatData.Messages, newMsg) + updatedData, err := json.MarshalIndent(chatData, "", " ") + if err != nil { + return fmt.Errorf("error encoding JSON: %v", err) + } + + if err := os.WriteFile(chatFile, updatedData, 0644); err != nil { + return fmt.Errorf("error writing chat file: %v", err) + } + + return nil +} + func LoadMessages(username string) { chatFile := filepath.Join("configs", "servers", "default", "users", strings.ToLower(username), "messages.json") data, err := os.ReadFile(chatFile) diff --git a/pkg/clean_image/clean_image.go b/pkg/clean_image/clean_image.go new file mode 100644 index 0000000..ba1eafa --- /dev/null +++ b/pkg/clean_image/clean_image.go @@ -0,0 +1,257 @@ +package cleanimage + +import ( + "fmt" + "strings" +) + +// KittyImageCleaner provides methods to generate Kitty graphics protocol +// commands for deleting images. +type KittyImageCleaner struct{} + +// NewKittyImageCleaner creates a new instance of KittyImageCleaner. +func NewKittyImageCleaner() *KittyImageCleaner { + return &KittyImageCleaner{} +} + +// buildCommand constructs the base Kitty graphics protocol command. +func (kic *KittyImageCleaner) buildCommand(params map[string]string) string { + var sb strings.Builder + sb.WriteString("\033_Ga=d") // Start with the delete action + + if len(params) > 0 { + var paramStrings []string + for key, value := range params { + paramStrings = append(paramStrings, fmt.Sprintf("%s=%s", key, value)) + } + sb.WriteString(",") + sb.WriteString(strings.Join(paramStrings, ",")) + } + + sb.WriteString("\033\\") // End the command + return sb.String() +} + +// DeleteAllVisiblePlacements deletes all images visible on screen. +// 'freeData' determines if the underlying image data should also be freed. +func (kic *KittyImageCleaner) DeleteAllVisiblePlacements(freeData bool) string { + if freeData { + return kic.buildCommand(map[string]string{"d": "A"}) + } + return kic.buildCommand(map[string]string{"d": "a"}) +} + +// DeleteByID deletes images with a specific ID. +// 'imageID' is the ID of the image to delete. +// 'placementID' is an optional placement ID. If 0, all placements with the imageID are deleted. +// 'freeData' determines if the underlying image data should also be freed. +func (kic *KittyImageCleaner) DeleteByID(imageID int, placementID int, freeData bool) string { + params := make(map[string]string) + if freeData { + params["d"] = "I" + } else { + params["d"] = "i" + } + params["i"] = fmt.Sprintf("%d", imageID) + if placementID != 0 { + params["p"] = fmt.Sprintf("%d", placementID) + } + return kic.buildCommand(params) +} + +// DeleteNewestByID deletes the newest image with a specified number (ID). +// 'imageNumber' is the number (ID) of the newest image to delete. +// 'placementID' is an optional placement ID. If 0, all placements with the imageNumber are deleted. +// 'freeData' determines if the underlying image data should also be freed. +func (kic *KittyImageCleaner) DeleteNewestByID(imageNumber int, placementID int, freeData bool) string { + params := make(map[string]string) + if freeData { + params["d"] = "N" + } else { + params["d"] = "n" + } + params["I"] = fmt.Sprintf("%d", imageNumber) // Note: Kitty uses 'I' for number here + if placementID != 0 { + params["p"] = fmt.Sprintf("%d", placementID) + } + return kic.buildCommand(params) +} + +// DeleteByCursorPosition deletes all placements that intersect with the current cursor position. +// 'freeData' determines if the underlying image data should also be freed. +func (kic *KittyImageCleaner) DeleteByCursorPosition(freeData bool) string { + if freeData { + return kic.buildCommand(map[string]string{"d": "C"}) + } + return kic.buildCommand(map[string]string{"d": "c"}) +} + +// DeleteAnimationFrames deletes animation frames. +// 'freeData' determines if the underlying image data should also be freed. +func (kic *KittyImageCleaner) DeleteAnimationFrames(freeData bool) string { + if freeData { + return kic.buildCommand(map[string]string{"d": "F"}) + } + return kic.buildCommand(map[string]string{"d": "f"}) +} + +// DeleteByCellPosition deletes all placements that intersect a specific cell. +// 'x', 'y' are the coordinates of the cell (1-indexed). +// 'freeData' determines if the underlying image data should also be freed. +func (kic *KittyImageCleaner) DeleteByCellPosition(x, y int, freeData bool) string { + params := map[string]string{ + "x": fmt.Sprintf("%d", x), + "y": fmt.Sprintf("%d", y), + } + if freeData { + params["d"] = "P" + } else { + params["d"] = "p" + } + return kic.buildCommand(params) +} + +// DeleteByCellAndZIndex deletes all placements that intersect a specific cell +// and have a specific z-index. +// 'x', 'y' are the coordinates of the cell (1-indexed). +// 'zIndex' is the z-index of the placements to delete. +// 'freeData' determines if the underlying image data should also be freed. +func (kic *KittyImageCleaner) DeleteByCellAndZIndex(x, y, zIndex int, freeData bool) string { + params := map[string]string{ + "x": fmt.Sprintf("%d", x), + "y": fmt.Sprintf("%d", y), + "z": fmt.Sprintf("%d", zIndex), + } + if freeData { + params["d"] = "Q" + } else { + params["d"] = "q" + } + return kic.buildCommand(params) +} + +// DeleteByIDRange deletes all images whose ID is within a specified range. +// 'minID' is the minimum ID (inclusive). +// 'maxID' is the maximum ID (inclusive). +// 'freeData' determines if the underlying image data should also be freed. +// (Requires Kitty version 0.33.0 or later) +func (kic *KittyImageCleaner) DeleteByIDRange(minID, maxID int, freeData bool) string { + params := map[string]string{ + "x": fmt.Sprintf("%d", minID), + "y": fmt.Sprintf("%d", maxID), + } + if freeData { + params["d"] = "R" + } else { + params["d"] = "r" + } + return kic.buildCommand(params) +} + +// DeleteByColumn deletes all placements that intersect the specified column. +// 'column' is the column number (1-indexed). +// 'freeData' determines if the underlying image data should also be freed. +func (kic *KittyImageCleaner) DeleteByColumn(column int, freeData bool) string { + params := map[string]string{ + "x": fmt.Sprintf("%d", column), + } + if freeData { + params["d"] = "X" + } else { + params["d"] = "x" + } + return kic.buildCommand(params) +} + +// DeleteByRow deletes all placements that intersect the specified row. +// 'row' is the row number (1-indexed). +// 'freeData' determines if the underlying image data should also be freed. +func (kic *KittyImageCleaner) DeleteByRow(row int, freeData bool) string { + params := map[string]string{ + "y": fmt.Sprintf("%d", row), + } + if freeData { + params["d"] = "Y" + } else { + params["d"] = "y" + } + return kic.buildCommand(params) +} + +// DeleteByZIndex deletes all placements that have the specified z-index. +// 'zIndex' is the z-index of the placements to delete. +// 'freeData' determines if the underlying image data should also be freed. +func (kic *KittyImageCleaner) DeleteByZIndex(zIndex int, freeData bool) string { + params := map[string]string{ + "z": fmt.Sprintf("%d", zIndex), + } + if freeData { + params["d"] = "Z" + } else { + params["d"] = "z" + } + return kic.buildCommand(params) +} + +func main() { + cleaner := NewKittyImageCleaner() + + // Example usage: + fmt.Println("Kitty Image Cleaning Commands:") + fmt.Println("-------------------------------") + + // _Ga=d\ # delete all visible placements + fmt.Println("Delete all visible placements (no data free):", + cleaner.DeleteAllVisiblePlacements(false)) + + // _Ga=d,d=A\ # delete all visible placements, freeing data + fmt.Println("Delete all visible placements (with data free):", + cleaner.DeleteAllVisiblePlacements(true)) + + // _Ga=d,d=i,i=10\ # delete the image with id=10, without freeing data + fmt.Println("Delete image with ID 10 (no data free):", + cleaner.DeleteByID(10, 0, false)) + + // _Ga=d,d=I,i=10\ # delete the image with id=10, freeing data + fmt.Println("Delete image with ID 10 (with data free):", + cleaner.DeleteByID(10, 0, true)) + + // _Ga=d,d=i,i=10,p=7\ # delete the image with id=10 and placement id=7, without freeing data + fmt.Println("Delete placement 7 of image ID 10 (no data free):", + cleaner.DeleteByID(10, 7, false)) + + // _Ga=d,d=I,i=10,p=7\ # delete the image with id=10 and placement id=7, freeing data + fmt.Println("Delete placement 7 of image ID 10 (with data free):", + cleaner.DeleteByID(10, 7, true)) + + // _Ga=d,d=Z,z=-1\ # delete the placements with z-index -1, also freeing up image data + fmt.Println("Delete placements with z-index -1 (with data free):", + cleaner.DeleteByZIndex(-1, true)) + + // _Ga=d,d=z,z=0\ # delete the placements with z-index 0, without freeing data + fmt.Println("Delete placements with z-index 0 (no data free):", + cleaner.DeleteByZIndex(0, false)) + + // _Ga=d,d=p,x=3,y=4\ # delete all placements that intersect the cell at (3, 4), without freeing data + fmt.Println("Delete placements at cell (3,4) (no data free):", + cleaner.DeleteByCellPosition(3, 4, false)) + + // _Ga=d,d=P,x=5,y=6\ # delete all placements that intersect the cell at (5, 6), freeing data + fmt.Println("Delete placements at cell (5,6) (with data free):", + cleaner.DeleteByCellPosition(5, 6, true)) + + fmt.Println("Delete placements intersecting cursor (no data free):", + cleaner.DeleteByCursorPosition(false)) + + fmt.Println("Delete placements intersecting column 10 (with data free):", + cleaner.DeleteByColumn(10, true)) + + fmt.Println("Delete placements intersecting row 5 (no data free):", + cleaner.DeleteByRow(5, false)) + + fmt.Println("Delete images with ID range 100-200 (with data free):", + cleaner.DeleteByIDRange(100, 200, true)) + + fmt.Println("Delete placements at cell (1,1) with z-index 10 (no data free):", + cleaner.DeleteByCellAndZIndex(1, 1, 10, false)) +} \ No newline at end of file