mirror of
https://github.com/likelovewant/ollama-for-amd.git
synced 2025-12-26 00:18:02 +00:00
app: add code for macOS and Windows apps under 'app' (#12933)
* app: add code for macOS and Windows apps under 'app' * app: add readme * app: windows and linux only for now * ci: fix ui CI validation --------- Co-authored-by: jmorganca <jmorganca@gmail.com>
This commit is contained in:
15
app/dialog/LICENSE
Normal file
15
app/dialog/LICENSE
Normal file
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2018, the dialog authors.
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
43
app/dialog/cocoa/dlg.h
Normal file
43
app/dialog/cocoa/dlg.h
Normal file
@@ -0,0 +1,43 @@
|
||||
#include <objc/NSObjCRuntime.h>
|
||||
|
||||
typedef enum {
|
||||
MSG_YESNO,
|
||||
MSG_ERROR,
|
||||
MSG_INFO,
|
||||
} AlertStyle;
|
||||
|
||||
typedef struct {
|
||||
char* msg;
|
||||
char* title;
|
||||
AlertStyle style;
|
||||
} AlertDlgParams;
|
||||
|
||||
#define LOADDLG 0
|
||||
#define SAVEDLG 1
|
||||
#define DIRDLG 2 // browse for directory
|
||||
|
||||
typedef struct {
|
||||
int mode; /* which dialog style to invoke (see earlier defines) */
|
||||
char* buf; /* buffer to store selected file */
|
||||
int nbuf; /* number of bytes allocated at buf */
|
||||
char* title; /* title for dialog box (can be nil) */
|
||||
void** exts; /* list of valid extensions (elements actual type is NSString*) */
|
||||
int numext; /* number of items in exts */
|
||||
int relaxext; /* allow other extensions? */
|
||||
char* startDir; /* directory to start in (can be nil) */
|
||||
char* filename; /* default filename for dialog box (can be nil) */
|
||||
int showHidden; /* show hidden files? */
|
||||
int allowMultiple; /* allow multiple file selection? */
|
||||
} FileDlgParams;
|
||||
|
||||
typedef enum {
|
||||
DLG_OK,
|
||||
DLG_CANCEL,
|
||||
DLG_URLFAIL,
|
||||
} DlgResult;
|
||||
|
||||
DlgResult alertDlg(AlertDlgParams*);
|
||||
DlgResult fileDlg(FileDlgParams*);
|
||||
|
||||
void* NSStr(void* buf, int len);
|
||||
void NSRelease(void* obj);
|
||||
195
app/dialog/cocoa/dlg.m
Normal file
195
app/dialog/cocoa/dlg.m
Normal file
@@ -0,0 +1,195 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#include "dlg.h"
|
||||
#include <string.h>
|
||||
#include <sys/syslimits.h>
|
||||
|
||||
void* NSStr(void* buf, int len) {
|
||||
return (void*)[[NSString alloc] initWithBytes:buf length:len encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
void checkActivationPolicy() {
|
||||
NSApplicationActivationPolicy policy = [NSApp activationPolicy];
|
||||
// prohibited NSApp will not show the panel at all.
|
||||
// It probably means that this is not run in a GUI app, that would set the policy on its own,
|
||||
// but in a terminal app - setting it to accessory will allow dialogs to show
|
||||
if (policy == NSApplicationActivationPolicyProhibited) {
|
||||
[NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
|
||||
}
|
||||
}
|
||||
|
||||
void NSRelease(void* obj) {
|
||||
[(NSObject*)obj release];
|
||||
}
|
||||
|
||||
@interface AlertDlg : NSObject {
|
||||
AlertDlgParams* params;
|
||||
DlgResult result;
|
||||
}
|
||||
+ (AlertDlg*)init:(AlertDlgParams*)params;
|
||||
- (DlgResult)run;
|
||||
@end
|
||||
|
||||
DlgResult alertDlg(AlertDlgParams* params) {
|
||||
return [[AlertDlg init:params] run];
|
||||
}
|
||||
|
||||
@implementation AlertDlg
|
||||
+ (AlertDlg*)init:(AlertDlgParams*)params {
|
||||
AlertDlg* d = [AlertDlg alloc];
|
||||
d->params = params;
|
||||
return d;
|
||||
}
|
||||
|
||||
- (DlgResult)run {
|
||||
if(![NSThread isMainThread]) {
|
||||
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES];
|
||||
return self->result;
|
||||
}
|
||||
NSAlert* alert = [[NSAlert alloc] init];
|
||||
if(self->params->title != nil) {
|
||||
[[alert window] setTitle:[[NSString alloc] initWithUTF8String:self->params->title]];
|
||||
}
|
||||
[alert setMessageText:[[NSString alloc] initWithUTF8String:self->params->msg]];
|
||||
switch (self->params->style) {
|
||||
case MSG_YESNO:
|
||||
[alert addButtonWithTitle:@"Yes"];
|
||||
[alert addButtonWithTitle:@"No"];
|
||||
break;
|
||||
case MSG_ERROR:
|
||||
[alert setIcon:[NSImage imageNamed:NSImageNameCaution]];
|
||||
[alert addButtonWithTitle:@"OK"];
|
||||
break;
|
||||
case MSG_INFO:
|
||||
[alert setIcon:[NSImage imageNamed:NSImageNameInfo]];
|
||||
[alert addButtonWithTitle:@"OK"];
|
||||
break;
|
||||
}
|
||||
|
||||
checkActivationPolicy();
|
||||
|
||||
self->result = [alert runModal] == NSAlertFirstButtonReturn ? DLG_OK : DLG_CANCEL;
|
||||
return self->result;
|
||||
}
|
||||
@end
|
||||
|
||||
@interface FileDlg : NSObject {
|
||||
FileDlgParams* params;
|
||||
DlgResult result;
|
||||
}
|
||||
+ (FileDlg*)init:(FileDlgParams*)params;
|
||||
- (DlgResult)run;
|
||||
@end
|
||||
|
||||
DlgResult fileDlg(FileDlgParams* params) {
|
||||
return [[FileDlg init:params] run];
|
||||
}
|
||||
|
||||
@implementation FileDlg
|
||||
+ (FileDlg*)init:(FileDlgParams*)params {
|
||||
FileDlg* d = [FileDlg alloc];
|
||||
d->params = params;
|
||||
return d;
|
||||
}
|
||||
|
||||
- (DlgResult)run {
|
||||
if(![NSThread isMainThread]) {
|
||||
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES];
|
||||
} else if(self->params->mode == SAVEDLG) {
|
||||
self->result = [self save];
|
||||
} else {
|
||||
self->result = [self load];
|
||||
}
|
||||
return self->result;
|
||||
}
|
||||
|
||||
- (NSInteger)runPanel:(NSSavePanel*)panel {
|
||||
[panel setFloatingPanel:YES];
|
||||
[panel setShowsHiddenFiles:self->params->showHidden ? YES : NO];
|
||||
[panel setCanCreateDirectories:YES];
|
||||
if(self->params->title != nil) {
|
||||
[panel setTitle:[[NSString alloc] initWithUTF8String:self->params->title]];
|
||||
}
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
if(self->params->numext > 0) {
|
||||
[panel setAllowedFileTypes:[NSArray arrayWithObjects:(NSString**)self->params->exts count:self->params->numext]];
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
if(self->params->relaxext) {
|
||||
[panel setAllowsOtherFileTypes:YES];
|
||||
}
|
||||
if(self->params->startDir) {
|
||||
[panel setDirectoryURL:[NSURL URLWithString:[[NSString alloc] initWithUTF8String:self->params->startDir]]];
|
||||
}
|
||||
if(self->params->filename != nil) {
|
||||
[panel setNameFieldStringValue:[[NSString alloc] initWithUTF8String:self->params->filename]];
|
||||
}
|
||||
|
||||
checkActivationPolicy();
|
||||
|
||||
return [panel runModal];
|
||||
}
|
||||
|
||||
- (DlgResult)save {
|
||||
NSSavePanel* panel = [NSSavePanel savePanel];
|
||||
if(![self runPanel:panel]) {
|
||||
return DLG_CANCEL;
|
||||
} else if(![[panel URL] getFileSystemRepresentation:self->params->buf maxLength:self->params->nbuf]) {
|
||||
return DLG_URLFAIL;
|
||||
}
|
||||
return DLG_OK;
|
||||
}
|
||||
|
||||
- (DlgResult)load {
|
||||
NSOpenPanel* panel = [NSOpenPanel openPanel];
|
||||
if(self->params->mode == DIRDLG) {
|
||||
[panel setCanChooseDirectories:YES];
|
||||
[panel setCanChooseFiles:NO];
|
||||
}
|
||||
|
||||
if(self->params->allowMultiple) {
|
||||
[panel setAllowsMultipleSelection:YES];
|
||||
}
|
||||
|
||||
if(![self runPanel:panel]) {
|
||||
return DLG_CANCEL;
|
||||
}
|
||||
|
||||
NSArray* urls = [panel URLs];
|
||||
if(self->params->allowMultiple && [urls count] >= 1) {
|
||||
// For multiple files, we need to return all paths separated by null bytes
|
||||
char* bufPtr = self->params->buf;
|
||||
int remainingBuf = self->params->nbuf;
|
||||
|
||||
// Calculate total required buffer size first
|
||||
int totalSize = 0;
|
||||
for(NSURL* url in urls) {
|
||||
char tempBuf[PATH_MAX];
|
||||
if(![url getFileSystemRepresentation:tempBuf maxLength:PATH_MAX]) {
|
||||
return DLG_URLFAIL;
|
||||
}
|
||||
totalSize += strlen(tempBuf) + 1; // +1 for null terminator
|
||||
}
|
||||
totalSize += 1; // Final null terminator
|
||||
|
||||
if(totalSize > self->params->nbuf) {
|
||||
// Not enough buffer space
|
||||
return DLG_URLFAIL;
|
||||
}
|
||||
|
||||
// Now actually copy the paths (we know we have space)
|
||||
bufPtr = self->params->buf;
|
||||
for(NSURL* url in urls) {
|
||||
char tempBuf[PATH_MAX];
|
||||
[url getFileSystemRepresentation:tempBuf maxLength:PATH_MAX];
|
||||
int pathLen = strlen(tempBuf);
|
||||
strcpy(bufPtr, tempBuf);
|
||||
bufPtr += pathLen + 1;
|
||||
}
|
||||
*bufPtr = '\0'; // Final null terminator
|
||||
}
|
||||
|
||||
return DLG_OK;
|
||||
}
|
||||
|
||||
@end
|
||||
183
app/dialog/cocoa/dlg_darwin.go
Normal file
183
app/dialog/cocoa/dlg_darwin.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package cocoa
|
||||
|
||||
// #cgo darwin LDFLAGS: -framework Cocoa
|
||||
// #include <stdlib.h>
|
||||
// #include <sys/syslimits.h>
|
||||
// #include "dlg.h"
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type AlertParams struct {
|
||||
p C.AlertDlgParams
|
||||
}
|
||||
|
||||
func mkAlertParams(msg, title string, style C.AlertStyle) *AlertParams {
|
||||
a := AlertParams{C.AlertDlgParams{msg: C.CString(msg), style: style}}
|
||||
if title != "" {
|
||||
a.p.title = C.CString(title)
|
||||
}
|
||||
return &a
|
||||
}
|
||||
|
||||
func (a *AlertParams) run() C.DlgResult {
|
||||
return C.alertDlg(&a.p)
|
||||
}
|
||||
|
||||
func (a *AlertParams) free() {
|
||||
C.free(unsafe.Pointer(a.p.msg))
|
||||
if a.p.title != nil {
|
||||
C.free(unsafe.Pointer(a.p.title))
|
||||
}
|
||||
}
|
||||
|
||||
func nsStr(s string) unsafe.Pointer {
|
||||
return C.NSStr(unsafe.Pointer(&[]byte(s)[0]), C.int(len(s)))
|
||||
}
|
||||
|
||||
func YesNoDlg(msg, title string) bool {
|
||||
a := mkAlertParams(msg, title, C.MSG_YESNO)
|
||||
defer a.free()
|
||||
return a.run() == C.DLG_OK
|
||||
}
|
||||
|
||||
func InfoDlg(msg, title string) {
|
||||
a := mkAlertParams(msg, title, C.MSG_INFO)
|
||||
defer a.free()
|
||||
a.run()
|
||||
}
|
||||
|
||||
func ErrorDlg(msg, title string) {
|
||||
a := mkAlertParams(msg, title, C.MSG_ERROR)
|
||||
defer a.free()
|
||||
a.run()
|
||||
}
|
||||
|
||||
const (
|
||||
BUFSIZE = C.PATH_MAX
|
||||
MULTI_FILE_BUF_SIZE = 32768
|
||||
)
|
||||
|
||||
// MultiFileDlg opens a file dialog that allows multiple file selection
|
||||
func MultiFileDlg(title string, exts []string, relaxExt bool, startDir string, showHidden bool) ([]string, error) {
|
||||
return fileDlgWithOptions(C.LOADDLG, title, exts, relaxExt, startDir, "", showHidden, true)
|
||||
}
|
||||
|
||||
// FileDlg opens a file dialog for single file selection (kept for compatibility)
|
||||
func FileDlg(save bool, title string, exts []string, relaxExt bool, startDir string, filename string, showHidden bool) (string, error) {
|
||||
mode := C.LOADDLG
|
||||
if save {
|
||||
mode = C.SAVEDLG
|
||||
}
|
||||
files, err := fileDlgWithOptions(mode, title, exts, relaxExt, startDir, filename, showHidden, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return files[0], nil
|
||||
}
|
||||
|
||||
func DirDlg(title string, startDir string, showHidden bool) (string, error) {
|
||||
files, err := fileDlgWithOptions(C.DIRDLG, title, nil, false, startDir, "", showHidden, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return files[0], nil
|
||||
}
|
||||
|
||||
// fileDlgWithOptions is the unified file dialog function that handles both single and multiple selection
|
||||
func fileDlgWithOptions(mode int, title string, exts []string, relaxExt bool, startDir, filename string, showHidden, allowMultiple bool) ([]string, error) {
|
||||
// Use larger buffer for multiple files, smaller for single
|
||||
bufSize := BUFSIZE
|
||||
if allowMultiple {
|
||||
bufSize = MULTI_FILE_BUF_SIZE
|
||||
}
|
||||
|
||||
p := C.FileDlgParams{
|
||||
mode: C.int(mode),
|
||||
nbuf: C.int(bufSize),
|
||||
}
|
||||
|
||||
if allowMultiple {
|
||||
p.allowMultiple = C.int(1) // Enable multiple selection //nolint:structcheck
|
||||
}
|
||||
if showHidden {
|
||||
p.showHidden = 1
|
||||
}
|
||||
|
||||
p.buf = (*C.char)(C.malloc(C.size_t(bufSize)))
|
||||
defer C.free(unsafe.Pointer(p.buf))
|
||||
buf := (*(*[MULTI_FILE_BUF_SIZE]byte)(unsafe.Pointer(p.buf)))[:bufSize]
|
||||
|
||||
if title != "" {
|
||||
p.title = C.CString(title)
|
||||
defer C.free(unsafe.Pointer(p.title))
|
||||
}
|
||||
if startDir != "" {
|
||||
p.startDir = C.CString(startDir)
|
||||
defer C.free(unsafe.Pointer(p.startDir))
|
||||
}
|
||||
if filename != "" {
|
||||
p.filename = C.CString(filename)
|
||||
defer C.free(unsafe.Pointer(p.filename))
|
||||
}
|
||||
|
||||
if len(exts) > 0 {
|
||||
if len(exts) > 999 {
|
||||
panic("more than 999 extensions not supported")
|
||||
}
|
||||
ptrSize := int(unsafe.Sizeof(&title))
|
||||
p.exts = (*unsafe.Pointer)(C.malloc(C.size_t(ptrSize * len(exts))))
|
||||
defer C.free(unsafe.Pointer(p.exts))
|
||||
cext := (*(*[999]unsafe.Pointer)(unsafe.Pointer(p.exts)))[:]
|
||||
for i, ext := range exts {
|
||||
cext[i] = nsStr(ext)
|
||||
defer C.NSRelease(cext[i])
|
||||
}
|
||||
p.numext = C.int(len(exts))
|
||||
if relaxExt {
|
||||
p.relaxext = 1
|
||||
}
|
||||
}
|
||||
|
||||
// Execute dialog and parse results
|
||||
switch C.fileDlg(&p) {
|
||||
case C.DLG_OK:
|
||||
if allowMultiple {
|
||||
// Parse multiple null-terminated strings from buffer
|
||||
var files []string
|
||||
start := 0
|
||||
for i := range len(buf) - 1 {
|
||||
if buf[i] == 0 {
|
||||
if i > start {
|
||||
files = append(files, string(buf[start:i]))
|
||||
}
|
||||
start = i + 1
|
||||
// Check for double null (end of list)
|
||||
if i+1 < len(buf) && buf[i+1] == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return files, nil
|
||||
} else {
|
||||
// Single file - return as array for consistency
|
||||
filename := string(buf[:bytes.Index(buf, []byte{0})])
|
||||
return []string{filename}, nil
|
||||
}
|
||||
case C.DLG_CANCEL:
|
||||
return nil, nil
|
||||
case C.DLG_URLFAIL:
|
||||
return nil, errors.New("failed to get file-system representation for selected URL")
|
||||
}
|
||||
panic("unhandled case")
|
||||
}
|
||||
182
app/dialog/dlgs.go
Normal file
182
app/dialog/dlgs.go
Normal file
@@ -0,0 +1,182 @@
|
||||
//go:build windows || darwin
|
||||
|
||||
// Package dialog provides a simple cross-platform common dialog API.
|
||||
// Eg. to prompt the user with a yes/no dialog:
|
||||
//
|
||||
// if dialog.MsgDlg("%s", "Do you want to continue?").YesNo() {
|
||||
// // user pressed Yes
|
||||
// }
|
||||
//
|
||||
// The general usage pattern is to call one of the toplevel *Dlg functions
|
||||
// which return a *Builder structure. From here you can optionally call
|
||||
// configuration functions (eg. Title) to customise the dialog, before
|
||||
// using a launcher function to run the dialog.
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ErrCancelled is an error returned when a user cancels/closes a dialog.
|
||||
var ErrCancelled = errors.New("Cancelled")
|
||||
|
||||
// Cancelled refers to ErrCancelled.
|
||||
// Deprecated: Use ErrCancelled instead.
|
||||
var Cancelled = ErrCancelled
|
||||
|
||||
// Dlg is the common type for dialogs.
|
||||
type Dlg struct {
|
||||
Title string
|
||||
}
|
||||
|
||||
// MsgBuilder is used for creating message boxes.
|
||||
type MsgBuilder struct {
|
||||
Dlg
|
||||
Msg string
|
||||
}
|
||||
|
||||
// Message initialises a MsgBuilder with the provided message.
|
||||
func Message(format string, args ...interface{}) *MsgBuilder {
|
||||
return &MsgBuilder{Msg: fmt.Sprintf(format, args...)}
|
||||
}
|
||||
|
||||
// Title specifies what the title of the message dialog will be.
|
||||
func (b *MsgBuilder) Title(title string) *MsgBuilder {
|
||||
b.Dlg.Title = title
|
||||
return b
|
||||
}
|
||||
|
||||
// YesNo spawns the message dialog with two buttons, "Yes" and "No".
|
||||
// Returns true iff the user selected "Yes".
|
||||
func (b *MsgBuilder) YesNo() bool {
|
||||
return b.yesNo()
|
||||
}
|
||||
|
||||
// Info spawns the message dialog with an information icon and single button, "Ok".
|
||||
func (b *MsgBuilder) Info() {
|
||||
b.info()
|
||||
}
|
||||
|
||||
// Error spawns the message dialog with an error icon and single button, "Ok".
|
||||
func (b *MsgBuilder) Error() {
|
||||
b.error()
|
||||
}
|
||||
|
||||
// FileFilter represents a category of files (eg. audio files, spreadsheets).
|
||||
type FileFilter struct {
|
||||
Desc string
|
||||
Extensions []string
|
||||
}
|
||||
|
||||
// FileBuilder is used for creating file browsing dialogs.
|
||||
type FileBuilder struct {
|
||||
Dlg
|
||||
StartDir string
|
||||
StartFile string
|
||||
Filters []FileFilter
|
||||
ShowHiddenFiles bool
|
||||
}
|
||||
|
||||
// File initialises a FileBuilder using the default configuration.
|
||||
func File() *FileBuilder {
|
||||
return &FileBuilder{}
|
||||
}
|
||||
|
||||
// Title specifies the title to be used for the dialog.
|
||||
func (b *FileBuilder) Title(title string) *FileBuilder {
|
||||
b.Dlg.Title = title
|
||||
return b
|
||||
}
|
||||
|
||||
// Filter adds a category of files to the types allowed by the dialog. Multiple
|
||||
// calls to Filter are cumulative - any of the provided categories will be allowed.
|
||||
// By default all files can be selected.
|
||||
//
|
||||
// The special extension '*' allows all files to be selected when the Filter is active.
|
||||
func (b *FileBuilder) Filter(desc string, extensions ...string) *FileBuilder {
|
||||
filt := FileFilter{desc, extensions}
|
||||
if len(filt.Extensions) == 0 {
|
||||
filt.Extensions = append(filt.Extensions, "*")
|
||||
}
|
||||
b.Filters = append(b.Filters, filt)
|
||||
return b
|
||||
}
|
||||
|
||||
// SetStartDir specifies the initial directory of the dialog.
|
||||
func (b *FileBuilder) SetStartDir(startDir string) *FileBuilder {
|
||||
b.StartDir = startDir
|
||||
return b
|
||||
}
|
||||
|
||||
// SetStartFile specifies the initial file name of the dialog.
|
||||
func (b *FileBuilder) SetStartFile(startFile string) *FileBuilder {
|
||||
b.StartFile = startFile
|
||||
return b
|
||||
}
|
||||
|
||||
// ShowHiddenFiles sets whether hidden files should be visible in the dialog.
|
||||
func (b *FileBuilder) ShowHidden(show bool) *FileBuilder {
|
||||
b.ShowHiddenFiles = show
|
||||
return b
|
||||
}
|
||||
|
||||
// Load spawns the file selection dialog using the configured settings,
|
||||
// asking the user to select a single file. Returns ErrCancelled as the error
|
||||
// if the user cancels or closes the dialog.
|
||||
func (b *FileBuilder) Load() (string, error) {
|
||||
return b.load()
|
||||
}
|
||||
|
||||
// LoadMultiple spawns the file selection dialog using the configured settings,
|
||||
// asking the user to select multiple files. Returns ErrCancelled as the error
|
||||
// if the user cancels or closes the dialog.
|
||||
func (b *FileBuilder) LoadMultiple() ([]string, error) {
|
||||
return b.loadMultiple()
|
||||
}
|
||||
|
||||
// Save spawns the file selection dialog using the configured settings,
|
||||
// asking the user for a filename to save as. If the chosen file exists, the
|
||||
// user is prompted whether they want to overwrite the file. Returns
|
||||
// ErrCancelled as the error if the user cancels/closes the dialog, or selects
|
||||
// not to overwrite the file.
|
||||
func (b *FileBuilder) Save() (string, error) {
|
||||
return b.save()
|
||||
}
|
||||
|
||||
// DirectoryBuilder is used for directory browse dialogs.
|
||||
type DirectoryBuilder struct {
|
||||
Dlg
|
||||
StartDir string
|
||||
ShowHiddenFiles bool
|
||||
}
|
||||
|
||||
// Directory initialises a DirectoryBuilder using the default configuration.
|
||||
func Directory() *DirectoryBuilder {
|
||||
return &DirectoryBuilder{}
|
||||
}
|
||||
|
||||
// Browse spawns the directory selection dialog using the configured settings,
|
||||
// asking the user to select a single folder. Returns ErrCancelled as the error
|
||||
// if the user cancels or closes the dialog.
|
||||
func (b *DirectoryBuilder) Browse() (string, error) {
|
||||
return b.browse()
|
||||
}
|
||||
|
||||
// Title specifies the title to be used for the dialog.
|
||||
func (b *DirectoryBuilder) Title(title string) *DirectoryBuilder {
|
||||
b.Dlg.Title = title
|
||||
return b
|
||||
}
|
||||
|
||||
// StartDir specifies the initial directory to be used for the dialog.
|
||||
func (b *DirectoryBuilder) SetStartDir(dir string) *DirectoryBuilder {
|
||||
b.StartDir = dir
|
||||
return b
|
||||
}
|
||||
|
||||
// ShowHiddenFiles sets whether hidden files should be visible in the dialog.
|
||||
func (b *DirectoryBuilder) ShowHidden(show bool) *DirectoryBuilder {
|
||||
b.ShowHiddenFiles = show
|
||||
return b
|
||||
}
|
||||
82
app/dialog/dlgs_darwin.go
Normal file
82
app/dialog/dlgs_darwin.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"github.com/ollama/ollama/app/dialog/cocoa"
|
||||
)
|
||||
|
||||
func (b *MsgBuilder) yesNo() bool {
|
||||
return cocoa.YesNoDlg(b.Msg, b.Dlg.Title)
|
||||
}
|
||||
|
||||
func (b *MsgBuilder) info() {
|
||||
cocoa.InfoDlg(b.Msg, b.Dlg.Title)
|
||||
}
|
||||
|
||||
func (b *MsgBuilder) error() {
|
||||
cocoa.ErrorDlg(b.Msg, b.Dlg.Title)
|
||||
}
|
||||
|
||||
func (b *FileBuilder) load() (string, error) {
|
||||
return b.run(false)
|
||||
}
|
||||
|
||||
func (b *FileBuilder) loadMultiple() ([]string, error) {
|
||||
return b.runMultiple()
|
||||
}
|
||||
|
||||
func (b *FileBuilder) save() (string, error) {
|
||||
return b.run(true)
|
||||
}
|
||||
|
||||
func (b *FileBuilder) run(save bool) (string, error) {
|
||||
star := false
|
||||
var exts []string
|
||||
for _, filt := range b.Filters {
|
||||
for _, ext := range filt.Extensions {
|
||||
if ext == "*" {
|
||||
star = true
|
||||
} else {
|
||||
exts = append(exts, ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
if star && save {
|
||||
/* OSX doesn't allow the user to switch visible file types/extensions. Also
|
||||
** NSSavePanel's allowsOtherFileTypes property has no effect for an open
|
||||
** dialog, so if "*" is a possible extension we must always show all files. */
|
||||
exts = nil
|
||||
}
|
||||
f, err := cocoa.FileDlg(save, b.Dlg.Title, exts, star, b.StartDir, b.StartFile, b.ShowHiddenFiles)
|
||||
if f == "" && err == nil {
|
||||
return "", ErrCancelled
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
|
||||
func (b *FileBuilder) runMultiple() ([]string, error) {
|
||||
star := false
|
||||
var exts []string
|
||||
for _, filt := range b.Filters {
|
||||
for _, ext := range filt.Extensions {
|
||||
if ext == "*" {
|
||||
star = true
|
||||
} else {
|
||||
exts = append(exts, ext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
files, err := cocoa.MultiFileDlg(b.Dlg.Title, exts, star, b.StartDir, b.ShowHiddenFiles)
|
||||
if len(files) == 0 && err == nil {
|
||||
return nil, ErrCancelled
|
||||
}
|
||||
return files, err
|
||||
}
|
||||
|
||||
func (b *DirectoryBuilder) browse() (string, error) {
|
||||
f, err := cocoa.DirDlg(b.Dlg.Title, b.StartDir, b.ShowHiddenFiles)
|
||||
if f == "" && err == nil {
|
||||
return "", ErrCancelled
|
||||
}
|
||||
return f, err
|
||||
}
|
||||
241
app/dialog/dlgs_windows.go
Normal file
241
app/dialog/dlgs_windows.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package dialog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
|
||||
"github.com/TheTitanrain/w32"
|
||||
)
|
||||
|
||||
const multiFileBufferSize = w32.MAX_PATH * 10
|
||||
|
||||
type WinDlgError int
|
||||
|
||||
func (e WinDlgError) Error() string {
|
||||
return fmt.Sprintf("CommDlgExtendedError: %#x", e)
|
||||
}
|
||||
|
||||
func err() error {
|
||||
e := w32.CommDlgExtendedError()
|
||||
if e == 0 {
|
||||
return ErrCancelled
|
||||
}
|
||||
return WinDlgError(e)
|
||||
}
|
||||
|
||||
func (b *MsgBuilder) yesNo() bool {
|
||||
r := w32.MessageBox(w32.HWND(0), b.Msg, firstOf(b.Dlg.Title, "Confirm?"), w32.MB_YESNO)
|
||||
return r == w32.IDYES
|
||||
}
|
||||
|
||||
func (b *MsgBuilder) info() {
|
||||
w32.MessageBox(w32.HWND(0), b.Msg, firstOf(b.Dlg.Title, "Information"), w32.MB_OK|w32.MB_ICONINFORMATION)
|
||||
}
|
||||
|
||||
func (b *MsgBuilder) error() {
|
||||
w32.MessageBox(w32.HWND(0), b.Msg, firstOf(b.Dlg.Title, "Error"), w32.MB_OK|w32.MB_ICONERROR)
|
||||
}
|
||||
|
||||
type filedlg struct {
|
||||
buf []uint16
|
||||
filters []uint16
|
||||
opf *w32.OPENFILENAME
|
||||
}
|
||||
|
||||
func (d filedlg) Filename() string {
|
||||
i := 0
|
||||
for i < len(d.buf) && d.buf[i] != 0 {
|
||||
i++
|
||||
}
|
||||
return string(utf16.Decode(d.buf[:i]))
|
||||
}
|
||||
|
||||
func (d filedlg) parseMultipleFilenames() []string {
|
||||
var files []string
|
||||
i := 0
|
||||
|
||||
// Find first null terminator (directory path)
|
||||
for i < len(d.buf) && d.buf[i] != 0 {
|
||||
i++
|
||||
}
|
||||
|
||||
if i >= len(d.buf) {
|
||||
return files
|
||||
}
|
||||
|
||||
// Get directory path
|
||||
dirPath := string(utf16.Decode(d.buf[:i]))
|
||||
i++ // Skip null terminator
|
||||
|
||||
// Check if there are more files (multiple selection)
|
||||
if i < len(d.buf) && d.buf[i] != 0 {
|
||||
// Multiple files selected - parse filenames
|
||||
for i < len(d.buf) {
|
||||
start := i
|
||||
// Find next null terminator
|
||||
for i < len(d.buf) && d.buf[i] != 0 {
|
||||
i++
|
||||
}
|
||||
if i >= len(d.buf) {
|
||||
break
|
||||
}
|
||||
|
||||
if start < i {
|
||||
filename := string(utf16.Decode(d.buf[start:i]))
|
||||
if dirPath != "" {
|
||||
files = append(files, dirPath+"\\"+filename)
|
||||
} else {
|
||||
files = append(files, filename)
|
||||
}
|
||||
}
|
||||
i++ // Skip null terminator
|
||||
if i >= len(d.buf) || d.buf[i] == 0 {
|
||||
break // End of list
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Single file selected
|
||||
files = append(files, dirPath)
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
func (b *FileBuilder) load() (string, error) {
|
||||
d := openfile(w32.OFN_FILEMUSTEXIST|w32.OFN_NOCHANGEDIR, b)
|
||||
if w32.GetOpenFileName(d.opf) {
|
||||
return d.Filename(), nil
|
||||
}
|
||||
return "", err()
|
||||
}
|
||||
|
||||
func (b *FileBuilder) loadMultiple() ([]string, error) {
|
||||
d := openfile(w32.OFN_FILEMUSTEXIST|w32.OFN_NOCHANGEDIR|w32.OFN_ALLOWMULTISELECT|w32.OFN_EXPLORER, b)
|
||||
d.buf = make([]uint16, multiFileBufferSize)
|
||||
d.opf.File = utf16ptr(d.buf)
|
||||
d.opf.MaxFile = uint32(len(d.buf))
|
||||
|
||||
if w32.GetOpenFileName(d.opf) {
|
||||
return d.parseMultipleFilenames(), nil
|
||||
}
|
||||
return nil, err()
|
||||
}
|
||||
|
||||
func (b *FileBuilder) save() (string, error) {
|
||||
d := openfile(w32.OFN_OVERWRITEPROMPT|w32.OFN_NOCHANGEDIR, b)
|
||||
if w32.GetSaveFileName(d.opf) {
|
||||
return d.Filename(), nil
|
||||
}
|
||||
return "", err()
|
||||
}
|
||||
|
||||
/* syscall.UTF16PtrFromString not sufficient because we need to encode embedded NUL bytes */
|
||||
func utf16ptr(utf16 []uint16) *uint16 {
|
||||
if utf16[len(utf16)-1] != 0 {
|
||||
panic("refusing to make ptr to non-NUL terminated utf16 slice")
|
||||
}
|
||||
h := (*reflect.SliceHeader)(unsafe.Pointer(&utf16))
|
||||
return (*uint16)(unsafe.Pointer(h.Data))
|
||||
}
|
||||
|
||||
func utf16slice(ptr *uint16) []uint16 { //nolint:unused
|
||||
hdr := reflect.SliceHeader{Data: uintptr(unsafe.Pointer(ptr)), Len: 1, Cap: 1}
|
||||
slice := *((*[]uint16)(unsafe.Pointer(&hdr))) //nolint:govet
|
||||
i := 0
|
||||
for slice[len(slice)-1] != 0 {
|
||||
i++
|
||||
}
|
||||
hdr.Len = i
|
||||
slice = *((*[]uint16)(unsafe.Pointer(&hdr))) //nolint:govet
|
||||
return slice
|
||||
}
|
||||
|
||||
func openfile(flags uint32, b *FileBuilder) (d filedlg) {
|
||||
d.buf = make([]uint16, w32.MAX_PATH)
|
||||
if b.StartFile != "" {
|
||||
initialName, _ := syscall.UTF16FromString(b.StartFile)
|
||||
for i := 0; i < len(initialName) && i < w32.MAX_PATH; i++ {
|
||||
d.buf[i] = initialName[i]
|
||||
}
|
||||
}
|
||||
d.opf = &w32.OPENFILENAME{
|
||||
File: utf16ptr(d.buf),
|
||||
MaxFile: uint32(len(d.buf)),
|
||||
Flags: flags,
|
||||
}
|
||||
d.opf.StructSize = uint32(unsafe.Sizeof(*d.opf))
|
||||
if b.StartDir != "" {
|
||||
d.opf.InitialDir, _ = syscall.UTF16PtrFromString(b.StartDir)
|
||||
}
|
||||
if b.Dlg.Title != "" {
|
||||
d.opf.Title, _ = syscall.UTF16PtrFromString(b.Dlg.Title)
|
||||
}
|
||||
for _, filt := range b.Filters {
|
||||
/* build utf16 string of form "Music File\0*.mp3;*.ogg;*.wav;\0" */
|
||||
d.filters = append(d.filters, utf16.Encode([]rune(filt.Desc))...)
|
||||
d.filters = append(d.filters, 0)
|
||||
for _, ext := range filt.Extensions {
|
||||
s := fmt.Sprintf("*.%s;", ext)
|
||||
d.filters = append(d.filters, utf16.Encode([]rune(s))...)
|
||||
}
|
||||
d.filters = append(d.filters, 0)
|
||||
}
|
||||
if d.filters != nil {
|
||||
d.filters = append(d.filters, 0, 0) // two extra NUL chars to terminate the list
|
||||
d.opf.Filter = utf16ptr(d.filters)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
type dirdlg struct {
|
||||
bi *w32.BROWSEINFO
|
||||
}
|
||||
|
||||
const (
|
||||
bffm_INITIALIZED = 1
|
||||
bffm_SELCHANGED = 2
|
||||
bffm_VALIDATEFAILEDA = 3
|
||||
bffm_VALIDATEFAILEDW = 4
|
||||
bffm_SETSTATUSTEXTA = (w32.WM_USER + 100)
|
||||
bffm_SETSTATUSTEXTW = (w32.WM_USER + 104)
|
||||
bffm_ENABLEOK = (w32.WM_USER + 101)
|
||||
bffm_SETSELECTIONA = (w32.WM_USER + 102)
|
||||
bffm_SETSELECTIONW = (w32.WM_USER + 103)
|
||||
bffm_SETOKTEXT = (w32.WM_USER + 105)
|
||||
bffm_SETEXPANDED = (w32.WM_USER + 106)
|
||||
bffm_SETSTATUSTEXT = bffm_SETSTATUSTEXTW
|
||||
bffm_SETSELECTION = bffm_SETSELECTIONW
|
||||
bffm_VALIDATEFAILED = bffm_VALIDATEFAILEDW
|
||||
)
|
||||
|
||||
func callbackDefaultDir(hwnd w32.HWND, msg uint, lParam, lpData uintptr) int {
|
||||
if msg == bffm_INITIALIZED {
|
||||
_ = w32.SendMessage(hwnd, bffm_SETSELECTION, w32.TRUE, lpData)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func selectdir(b *DirectoryBuilder) (d dirdlg) {
|
||||
d.bi = &w32.BROWSEINFO{Flags: w32.BIF_RETURNONLYFSDIRS | w32.BIF_NEWDIALOGSTYLE}
|
||||
if b.Dlg.Title != "" {
|
||||
d.bi.Title, _ = syscall.UTF16PtrFromString(b.Dlg.Title)
|
||||
}
|
||||
if b.StartDir != "" {
|
||||
s16, _ := syscall.UTF16PtrFromString(b.StartDir)
|
||||
d.bi.LParam = uintptr(unsafe.Pointer(s16))
|
||||
d.bi.CallbackFunc = syscall.NewCallback(callbackDefaultDir)
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (b *DirectoryBuilder) browse() (string, error) {
|
||||
d := selectdir(b)
|
||||
res := w32.SHBrowseForFolder(d.bi)
|
||||
if res == 0 {
|
||||
return "", ErrCancelled
|
||||
}
|
||||
return w32.SHGetPathFromIDList(res), nil
|
||||
}
|
||||
12
app/dialog/util.go
Normal file
12
app/dialog/util.go
Normal file
@@ -0,0 +1,12 @@
|
||||
//go:build windows
|
||||
|
||||
package dialog
|
||||
|
||||
func firstOf(args ...string) string {
|
||||
for _, arg := range args {
|
||||
if arg != "" {
|
||||
return arg
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user