Close to tray

Hi everybody. I created a solution for this Problem. Just today I updated my App. Hier can you find the Download GitHub - MichaelLepori/ObsiWizy: Tool for Obsidian Users and hier ObsiWizy On YouTube how it works. Enjoy it and let me know, if you would like to be able to do more with it. Till then, take care. ML

Hi! I’ve written an open-source plugin for Obsidian that adds this functionality (close to tray, open on startup, global hotkey): GitHub - dragonwocky/obsidian-tray: Launch Obsidian on startup and run it in the background from the system tray

I’ve submitted it to Obsidian’s community plugins repo, so hopefully it’ll be available there soon, but until then there are instructions for manual installation of the plugin at the link above.

11 Likes

Thanks to dragonwocky for writing the “Tray” plugin. Hoping to see it available as an official plugin soon!

3 Likes

This is such a great plugin! Thanks @dragonwocky!

i looked for it in the community plugins from within Obsidian, but it was not there under “Tray”.

This is a very significant UX addition for a lot of people, myself included. Please to the benevolent Obsidian masters, include this in the “official” community plugins section :pray::crossed_fingers::blush:

2 Likes

Close to tray, e.q. minimize as close opton will be great feature

Dragonwocky’s plugin doesn’t work that well for some Linux users, including myself. For us, this is still an issue and I still request the feature that names this thread.

(The plugin works beautifully on Windows, though)

1 Like

This would be nice!

@dragonwocky i’m using this plugin and work flawless :slight_smile:
Thanks a lot for the effort you put in this =)

1 Like

(post deleted by author)

There is an autohotkey script by @darsain in this thread. I extend it to add some value. Furthermore, I did this because the plugin by dragonwocky seems not be actively maintained (last release Sep 4, 2023).
Please keep in mind that the following autohotkey solution is only for windows.
Functionality: Bring obsidian to the foreground with the hotkey F2. The hotkey can be changed in the script (F2::). To minimize obsidian to tray, press F2 again (when obsidian is in the foreground). You can also click the minimize button on the top right of the obsidian window to minimize it to the tray.

Overall, there could be some edge cases where the script doesn’t work. The same limitations as those stated by darsain apply.

; Hide and restore obsidian to tray.
; Source: https://www.autohotkey.com/boards/viewtopic.php?p=607299#p607299
#Requires AutoHotkey v2.0

hider := HideToTray()

; Hide it at startup
Run(EnvGet("LocalAppData") . "\Programs\Obsidian\Obsidian.exe")
WinWait("ahk_exe Obsidian.exe", , 7)
Sleep(500)
hider.Hide(WinExist("ahk_exe Obsidian.exe"))



hidden_obsidian_window_id := false

#HotIf (GetKeyState("CapsLock", "P"))
2::
; The most fundamentals this block are from https://forum.obsidian.md/t/close-to-tray/6781/16.
{
    ; MsgBox(WinExist("ahk_exe Obsidian.exe"),, 4)
    global hidden_obsidian_window_id
    window_id := WinExist("ahk_exe Obsidian.exe")
    hider.RestoreLast()
    if (window_id) {
        ; Check if it's focused
        if (WinActive("ahk_id " . window_id)) {
            ; Save window id and hide
            hidden_obsidian_window_id := window_id
            hider.Hide(WinExist("ahk_exe Obsidian.exe"))
        } else {
            ; Focus
            WinActivate("ahk_id " . window_id)
        }
    } else if (hidden_obsidian_window_id)
    {
        ; Unhide
        hider.RestoreLast()
        hidden_obsidian_window_id := false
    } else if not WinActive("ahk_exe Obsidian.exe") {
        ; Launch the app
        A_LocalAppData := EnvGet("LocalAppData")
        Run(A_LocalAppData . "\Programs\Obsidian\Obsidian.exe")
        WinWait("ahk_exe Obsidian.exe", , 7)
        WinActivate("ahk_exe Obsidian.exe")
    }
}


class HideToTray
{
    ; add classes here to exclude from hiding:
    excludeClasses := ['Progman', 'WorkerW', 'Shell_TrayWnd', 'ApplicationFrameWindow']

    static __New() => SingletonDecorator.ApplyTo(this)

    __New() {
        this.test := ''
        this.exclude := Map()
        for winClass in this.excludeClasses {
            this.exclude[winClass] := ''
        }
        this.hiddenWindows := []
        this.knownWindows := Map()
        this.knownWindows.DefineProp('DeleteIfExists', { call: (s, p) => s.Has(p) && s.Delete(p) })
        this.SetHooks()
        Loop 2 {
            ObjRelease(ObjPtr(this))
        }
    }

    ; public methods
    Hide(hWnd) {
        if !this.exclude.Has(WinGetClass(hWnd)) {
            this.stopHook := true
            this.hiddenWindows.Push(HideToTray.HiddenWindow(hWnd))
            this.knownWindows[hWnd] := ''
            this.stopHook := false
        }
    }
    RestoreAll() => (this.stopHook := true, this.hiddenWindows := [], this.stopHook := false)
    RestoreLast() => this.hiddenWindows.Has(1) && this.hiddenWindows.Pop()

    ; private methods
    SetHooks() {
        static EVENT_SYSTEM_MINIMIZESTART := 0x0016
            , EVENT_SYSTEM_MINIMIZEEND := 0x0017
            , EVENT_OBJECT_DESTROY := 0x8001
            , OBJID_WINDOW := 0

        this.HasOwnProp('stopHook') || (
            this.stopHook := false,
            OnMessage(0x404, this.OnNotify := ObjBindMethod(this, 'AHK_NOTIFYICON')),
            this.destroyHook := WinEventHook(EVENT_OBJECT_DESTROY, EVENT_OBJECT_DESTROY, HookProc),
            this.minHook := WinEventHook(EVENT_SYSTEM_MINIMIZESTART, EVENT_SYSTEM_MINIMIZEEND, HookProc)
        )

        HookProc(hWinEventHook, event, hwnd, idObject, idChild, dwEventThread, dwmsEventTime) {
            if idObject = OBJID_WINDOW && !this.stopHook {
                switch event {
                    case EVENT_OBJECT_DESTROY: this.knownWindows.DeleteIfExists(hwnd), this.UnHide(hwnd)
                    case EVENT_SYSTEM_MINIMIZESTART: this.knownWindows.Has(hwnd) && this.Hide(hwnd)
                    case EVENT_SYSTEM_MINIMIZEEND: this.UnHide(hwnd)
                }
            }
        }
    }

    UnHide(hWnd) {
        try for i, wnd in this.hiddenWindows {
            continue
        } until wnd.hwnd = hwnd && this.hiddenWindows.RemoveAt(i)
    }

    AHK_NOTIFYICON(wp, lp, msg, hwnd) {
        static WM_LBUTTONDOWN := 0x201, WM_RBUTTONUP := 0x205
            , iconID := 0x404, iconMsg := 0x404, maxLenMenuStr := 40
        if !(lp = WM_LBUTTONDOWN || lp = WM_RBUTTONUP) {
            return
        }
        for i, wnd in this.hiddenWindows {
            if wnd.icon.wnd.hwnd = hwnd {
                switch lp {
                    case WM_RBUTTONUP: ShowMenu(i)
                    case WM_LBUTTONDOWN: this.hiddenWindows.RemoveAt(i)
                }
                break
            }
        }
        ShowMenu(idx) {
            title := this.hiddenWindows[idx].title
            b := StrLen(title) > maxLenMenuStr
            menuText := 'Restore «' . SubStr(title, 1, maxLenMenuStr) . (b ? '...' : '') . '»'
            iconMenu := Menu()
            iconMenu.Add(menuText, (*) => this.AHK_NOTIFYICON(iconID, WM_LBUTTONDOWN, iconMsg, hwnd))
            iconMenu.SetIcon(menuText, 'HICON:*' . this.hiddenWindows[idx].icon.hIcon)
            iconMenu.Add('Restore all windows', (*) => this.RestoreAll())
            iconMenu.Show()
            iconMenu.Delete()
        }
    }

    __Delete() {
        Loop 2 {
            ObjAddRef(ObjPtr(this))
        }
        this.DeleteProp('minHook')
        this.DeleteProp('destroyHook')
        OnMessage(0x404, this.OnNotify, 0)
        this.RestoreAll()
    }

    class HiddenWindow {
        __New(hWnd) {
            this.title := WinGetTitle(hWnd)
            this.icon := TrayIcon(this.GetWindowIcon(hWnd), this.title)
            WinMinimize(hWnd), WinHide(hWnd)
            this.hwnd := hWnd
        }

        __Delete() {
            try WinExist(this.hwnd) && (WinShow(), WinRestore(), WinActivate())
        }

        GetWindowIcon(hWnd) {
            static WM_GETICON := 0x007F, ICON_SMALL := 0, GCLP_HICONSM := -34
                , GetClassLong := 'GetClassLong' . (A_PtrSize = 4 ? '' : 'Ptr')
            ((hIcon := SendMessage(WM_GETICON, ICON_SMALL, A_ScreenDPI, , hWnd))
                || (hIcon := DllCall(GetClassLong, 'Ptr', hWnd, 'Int', GCLP_HICONSM, 'Ptr'))
                || (hIcon := LoadPicture('Shell32', 'Icon3', &IMAGE_ICON := 1)))
            return hIcon
        }
    }
}

class TrayIcon
{
    NIF_MESSAGE := 1, NIF_ICON := 2, NIF_TIP := 4
    NIM_ADD := 0, NIM_DELETE := 2
    iconID := 0x404, iconMsg := 0x404

    __New(hIcon, tip?) {
        flags := this.NIF_MESSAGE | this.NIF_ICON | (IsSet(tip) ? this.NIF_TIP : 0)
        this.wnd := Gui(), this.hIcon := hIcon
        this.NOTIFYICONDATA := Buffer(396, 0)
        NumPut(
            'Ptr', this.NOTIFYICONDATA.size,
            'Ptr', this.wnd.hwnd,
            'UInt', this.iconID,
            'UInt', flags,
            'Ptr', this.iconMsg,
            'Ptr', hIcon, this.NOTIFYICONDATA
        )
        if IsSet(tip) {
            StrPut(tip, this.NOTIFYICONDATA.ptr + 4 * A_PtrSize + 8, 'CP0')
        }
        DllCall('Shell32\Shell_NotifyIcon', 'UInt', this.NIM_ADD, 'Ptr', this.NOTIFYICONDATA)
    }

    __Delete() {
        DllCall('Shell32\Shell_NotifyIcon', 'UInt', this.NIM_DELETE, 'Ptr', this.NOTIFYICONDATA)
        this.wnd.Destroy()
    }
}

class WinEventHook
{
    ; Event Constants: https://is.gd/tRT5Wr
    __New(eventMin, eventMax, hookProc, options := '', idProcess := 0, idThread := 0, dwFlags := 0) {
        this.callback := CallbackCreate(hookProc, options, 7)
        this.hHook := DllCall('SetWinEventHook', 'UInt', eventMin, 'UInt', eventMax, 'Ptr', 0, 'Ptr', this.callback
            , 'UInt', idProcess, 'UInt', idThread, 'UInt', dwFlags, 'Ptr')
    }
    __Delete() {
        DllCall('UnhookWinEvent', 'Ptr', this.hHook)
        CallbackFree(this.callback)
    }
}

class SingletonDecorator
{
    static ApplyTo(targetClass) {
        proto := targetClass.Prototype
        origCall := targetClass.GetMethod('Call')
        origNew := proto.HasMethod('__New') ? proto.GetMethod('__New') : ''
        origDel := proto.HasMethod('__Delete') ? proto.GetMethod('__Delete') : ''

        targetClass.DefineProp('Call', { Call: (cls, p*) => cls.HasProp('singleton') ? cls.singleton : origCall(cls, p*) })
        proto.DefineProp('__New', { Call: (inst, p*) => (
            (origNew && origNew(inst, p*)),
            targetClass.singleton := inst,
            ObjRelease(ObjPtr(inst))
        ) })
        proto.DefineProp('__Delete', { Call: inst => (
            ObjAddRef(ObjPtr(inst)),
            (origDel && origDel(inst)),
            targetClass.DeleteProp('singleton')
        ) })
    }
}