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
100 changes: 50 additions & 50 deletions internal/ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import (
"charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
"golang.design/x/clipboard"
"github.com/gammons/slk/internal/cache"
"github.com/gammons/slk/internal/config"
"github.com/gammons/slk/internal/debuglog"
Expand All @@ -44,6 +43,7 @@ import (
"github.com/gammons/slk/internal/ui/threadsview"
"github.com/gammons/slk/internal/ui/workspace"
"github.com/gammons/slk/internal/ui/workspacefinder"
"golang.design/x/clipboard"
)

type Panel int
Expand Down Expand Up @@ -349,7 +349,7 @@ type (
UserID string
WorkspaceID string
}
TypingExpiredMsg struct{}
TypingExpiredMsg struct{}
PresenceChangeMsg struct {
UserID string
Presence string
Expand Down Expand Up @@ -694,9 +694,9 @@ type ChannelJoinFailedMsg struct {
// clipboard contents. Production code uses the real clipboard.Read.
type clipboardReader func(format clipboard.Format) []byte

// defaultClipboardReader is the real clipboard read function. It's
// defaultClipboardReader is the platform clipboard reader. It's
// overridable per-App via SetClipboardReader for tests.
var defaultClipboardReader clipboardReader = clipboard.Read
var defaultClipboardReader clipboardReader = platformClipboardReader()

type App struct {
// Sub-models
Expand Down Expand Up @@ -725,10 +725,10 @@ type App struct {
keys KeyMap

// Cached layout widths for mouse hit-testing
layoutRailWidth int
layoutSidebarEnd int // railWidth + sidebarWidth + sidebarBorder
layoutMsgEnd int // layoutSidebarEnd + msgWidth + msgBorder
layoutThreadEnd int // layoutMsgEnd + threadWidth + threadBorder
layoutRailWidth int
layoutSidebarEnd int // railWidth + sidebarWidth + sidebarBorder
layoutMsgEnd int // layoutSidebarEnd + msgWidth + msgBorder
layoutThreadEnd int // layoutMsgEnd + threadWidth + threadBorder
// Cached pane content heights, used for page-up/down distance calculations.
layoutMsgHeight int
layoutSidebarHeight int
Expand Down Expand Up @@ -774,11 +774,11 @@ type App struct {
// always).
channelSyncedAtReader func(channelID string) int64
olderMessagesFetcher OlderMessagesFetchFunc
messageSender MessageSendFunc
messageEditor MessageEditFunc
messageDeleter MessageDeleteFunc
messageMarkUnreader MarkUnreadFunc
uploader UploadFunc
messageSender MessageSendFunc
messageEditor MessageEditFunc
messageDeleter MessageDeleteFunc
messageMarkUnreader MarkUnreadFunc
uploader UploadFunc

// clipboardAvailable is set at startup based on the result of
// clipboard.Init(). When false, Ctrl+V smart-paste is a no-op.
Expand All @@ -788,18 +788,18 @@ type App struct {
// clipboard contents. Tests inject fakes via SetClipboardReader.
clipboardRead clipboardReader

threadFetcher ThreadFetchFunc
threadCacheReader ThreadCacheReadFunc
threadMarker ThreadMarkFunc
threadReplySender ThreadReplySendFunc
channelJoiner JoinChannelFunc
threadsListFetcher ThreadsListFetchFunc
threadFetcher ThreadFetchFunc
threadCacheReader ThreadCacheReadFunc
threadMarker ThreadMarkFunc
threadReplySender ThreadReplySendFunc
channelJoiner JoinChannelFunc
threadsListFetcher ThreadsListFetchFunc
// channelLastReadFetcher returns the parent channel's last_read_ts
// so the thread panel can render a "── new ──" boundary. Optional —
// when nil, the thread panel renders without an unread boundary.
channelLastReadFetcher func(channelID string) string
threadsDirtyDebounce time.Duration
fetchingOlder bool
threadsDirtyDebounce time.Duration
fetchingOlder bool

// Cached user-id -> display-name map (mirror of what SetUserNames
// last received). Used by openSelectedThreadCmd to populate the
Expand Down Expand Up @@ -1007,36 +1007,36 @@ func previewSpinnerTickCmd() tea.Cmd {

func NewApp() *App {
app := &App{
workspaceRail: workspace.New(nil, 0),
sidebar: sidebar.New(nil),
messagepane: messages.New(nil, ""),
compose: compose.New(""),
statusbar: statusbar.New(),
channelFinder: channelfinder.New(),
workspaceFinder: workspacefinder.New(),
themeSwitcher: themeswitcher.New(),
presenceMenu: presencemenu.New(),
help: help.New(),
threadPanel: thread.New(),
threadCompose: compose.New("thread"),
threadsView: threadsview.New(nil, ""),
reactionPicker: reactionpicker.New(),
confirmPrompt: confirmprompt.New(),
mode: ModeNormal,
focusedPanel: PanelSidebar,
sidebarVisible: true,
view: ViewChannels,
keys: DefaultKeyMap(),
typingUsers: make(map[string]map[string]time.Time),
selfSentTSes: make(map[string]time.Time),
workspaceRail: workspace.New(nil, 0),
sidebar: sidebar.New(nil),
messagepane: messages.New(nil, ""),
compose: compose.New(""),
statusbar: statusbar.New(),
channelFinder: channelfinder.New(),
workspaceFinder: workspacefinder.New(),
themeSwitcher: themeswitcher.New(),
presenceMenu: presencemenu.New(),
help: help.New(),
threadPanel: thread.New(),
threadCompose: compose.New("thread"),
threadsView: threadsview.New(nil, ""),
reactionPicker: reactionpicker.New(),
confirmPrompt: confirmprompt.New(),
mode: ModeNormal,
focusedPanel: PanelSidebar,
sidebarVisible: true,
view: ViewChannels,
keys: DefaultKeyMap(),
typingUsers: make(map[string]map[string]time.Time),
selfSentTSes: make(map[string]time.Time),
lastSelfSendByChannel: make(map[string]time.Time),
threadsDirtyDebounce: 150 * time.Millisecond,
userNames: map[string]string{},
externalUsers: map[string]bool{},
statusByTeam: map[string]workspaceStatus{},
lastChannelByTeam: map[string]string{},
navHistory: make(map[string]*navStack),
clipboardRead: defaultClipboardReader,
threadsDirtyDebounce: 150 * time.Millisecond,
userNames: map[string]string{},
externalUsers: map[string]bool{},
statusByTeam: map[string]workspaceStatus{},
lastChannelByTeam: map[string]string{},
navHistory: make(map[string]*navStack),
clipboardRead: defaultClipboardReader,
}
// Seed the picker with built-in emojis so the autocomplete works even
// before the first workspace finishes loading customs.
Expand Down
9 changes: 9 additions & 0 deletions internal/ui/clipboard_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build darwin && !cgo

package ui

import "golang.design/x/clipboard"

func platformClipboardReader() clipboardReader {
return clipboard.Read
}
91 changes: 91 additions & 0 deletions internal/ui/clipboard_darwin_cgo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
//go:build darwin && cgo

package ui

/*
#cgo CFLAGS: -x objective-c
#cgo LDFLAGS: -framework Foundation -framework Cocoa
#import <Foundation/Foundation.h>
#import <Cocoa/Cocoa.h>
#include <stdlib.h>

static unsigned int slk_copy_nsdata(NSData *data, void **out) {
if (data == nil || [data length] == 0) {
*out = NULL;
return 0;
}
NSUInteger size = [data length];
void *buf = malloc(size);
if (buf == NULL) {
*out = NULL;
return 0;
}
[data getBytes:buf length:size];
*out = buf;
return (unsigned int)size;
}

unsigned int slk_clipboard_read_image_png(void **out) {
@autoreleasepool {
*out = NULL;
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];

NSData *png = [pasteboard dataForType:NSPasteboardTypePNG];
if (png != nil) {
return slk_copy_nsdata(png, out);
}

NSData *tiff = [pasteboard dataForType:NSPasteboardTypeTIFF];
if (tiff != nil) {
NSBitmapImageRep *rep = [NSBitmapImageRep imageRepWithData:tiff];
NSData *converted = [rep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
if (converted != nil) {
return slk_copy_nsdata(converted, out);
}
}

NSImage *image = [[NSImage alloc] initWithPasteboard:pasteboard];
if (image == nil) {
return 0;
}
CGImageRef cgImage = [image CGImageForProposedRect:NULL context:nil hints:nil];
if (cgImage == NULL) {
[image release];
return 0;
}
NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage];
NSData *converted = [rep representationUsingType:NSBitmapImageFileTypePNG properties:@{}];
unsigned int n = slk_copy_nsdata(converted, out);
[rep release];
[image release];
return n;
}
}
*/
import "C"

import (
"unsafe"

"golang.design/x/clipboard"
)

func platformClipboardReader() clipboardReader {
return func(format clipboard.Format) []byte {
b := clipboard.Read(format)
if format != clipboard.FmtImage || len(b) > 0 {
return b
}
return readDarwinClipboardImagePNG()
}
}

func readDarwinClipboardImagePNG() []byte {
var data unsafe.Pointer
n := C.slk_clipboard_read_image_png(&data)
if data == nil || n == 0 {
return nil
}
defer C.free(data)
return C.GoBytes(data, C.int(n))
}
9 changes: 9 additions & 0 deletions internal/ui/clipboard_default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !darwin

package ui

import "golang.design/x/clipboard"

func platformClipboardReader() clipboardReader {
return clipboard.Read
}