Skip to content

Commit

Permalink
feat: cleanup stale marks
Browse files Browse the repository at this point in the history
  • Loading branch information
shalomb committed Nov 2, 2024
1 parent 15994d3 commit f60e5ce
Show file tree
Hide file tree
Showing 9 changed files with 137 additions and 70 deletions.
49 changes: 29 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
**unmarked 🎯**
---

Similar to [`harpoon`](https://github.com/ThePrimeagen/harpoon), unmarked is the keyboard user's tool for switching to windows
just using their marks.
Similar to [`harpoon`](https://github.com/ThePrimeagen/harpoon), unmarked
is the keyboard user's tool for switching desktop windows using just their
marks.

If you are familiar with [vim/neovim's concept of marks](https://vim.fandom.com/wiki/Using_marks#Setting_marks) - unmarked does
the same for desktop windows.

```shell
unmarked mark f # Give the currently active window a mark of f
unmarked mark f # Mark the currently active window with the letter 'f'
# Move around to other windows in the desktop environment, etc
unmarked summon f # Switch back to and focus the window with mark f
unmarked summon f # Switch back to and focus the window marked 'f'
```

[yabai](https://github.com/koekeishiya/yabai) and [skhd](https://github.com/koekeishiya/skhd) are required to complete functionality.
[yabai](https://github.com/koekeishiya/yabai) and [skhd](https://github.com/koekeishiya/skhd) are required to complete functionality. Works only on MacOS currently.

**Setup ⚙️**
---

With a `~/.config/skhd/skhdrc` file as follows

Expand All @@ -31,27 +38,29 @@ ctrl + alt + cmd - x : ~/.bin/unmarked mark x
ctrl + alt + cmd - y : ~/.bin/unmarked mark y
ctrl + alt + cmd - z : ~/.bin/unmarked mark z
```
You can press `ctrl-alt-cmd-t` when over a wezterm window to give it the mark
`t`, press `ctrl-alt-cmd-f` when over a firefox window to mark it with `f`,
etc, etc.
You are free to use any letter now to mark (and jump between) windows.

Let's say you use wezterm a lot in your workflow and want to mark it - you
would press `ctrl-alt-cmd-t` to mark it with the letter `t`. (`t` being
a mnemonic for terminal - but you would choose any letter of your liking).

At any point later, press `ctrl-alt-t` to raise/focus the wezterm
terminal, press `ctrl-alt-f` to focus firefox, etc.
Now, let's say you've switched windows and are doing something else and want
to move back to the wezterm window quickly - simply press `ctrl-alt-t`. Voila!

No need to `alt-tab` or reach for the mouse - Win! 🏆

**why? 💡**
---

My workflow usually involves making some code edits in neovim in wezterm,
switching to firefox to test, moving to jira to making some comments, moving
to teams to make an annoucement, moving back to neovim, etc. `alt-tabbing` my
way through these is a tad bit tedious.
Most developers' workflow usually involves making some code edits in the
terminal, switching to a browser to test, moving to some custom app to making
some comments, moving to slack to make an announcement, moving back to the
terminal to pick up coding work, etc.

My working set is usually 2-3 windows - and I want these to be quick to summon at the
speed of thought.
With unmarked, simply pressing `ctrl-alt-<mnemonic>` is enough to get me back
into the app and back on track.
`alt-tabbing` your way through many open windows is a tad bit tedious that the
tab key starts to develop a shine. For the few windows that make up the
current context, it should be super quick to switch to/between them and hence
the `ctrl-alt-<mnemonic>` to keep you in flow state.

**Building 🛠️**
---
Expand All @@ -60,10 +69,10 @@ Requires `go` >= 1.19, yabai, skhd

```shell
make build
cp unmarked-darwin ~/.bin/unmarked
cp unmarked-darwin* ~/.bin/unmarked # or some other dir in $PATH
export PATH="$HOME/.bin:$PATH"

unmarked help
unmarked help # Testing installation
```
**Debugging 🐞**
---
Expand Down
32 changes: 3 additions & 29 deletions choosecmd.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package main

import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"

log "github.com/sirupsen/logrus"
Expand All @@ -19,7 +16,7 @@ func init() {
// markCmd represents the mark command
var chooseMarksCmd = &cobra.Command{
Use: "choose",
Short: "Choose from available marked windows",
Short: "Choose from marked windows",
Long: `Choose from all the marked windows`,
Run: func(_ *cobra.Command, _ []string) {
log.Printf("choosing marks under %v", stateHome)
Expand All @@ -28,34 +25,11 @@ var chooseMarksCmd = &cobra.Command{
}

func chooseMarks() {
matches, _ := filepath.Glob(fmt.Sprintf("%s/*", stateHome))
lines := []string{}
for _, match := range matches {
f, _ := os.Stat(match)
if !f.IsDir() {
data, err := os.ReadFile(match)
if err != nil {
log.Printf("ERROR: could not read file", match)
}

app, err := jq(".app", string(data))
if err != nil {
log.Printf("Could not find .app under %v", data)
}

title, err := jq(".title", string(data))
if err != nil {
log.Printf("Could not find .title under %v", data)
}

line := fmt.Sprintf("%s -> %s [%s]\n", filepath.Base(match), title, app)
lines = append(lines, line)
}
}
lines, _ := findMarks()

chooseCmd := exec.Command(
"choose", "-n", "20", "-s", "20", "-b", "fac898",
"-c", "FF7518", "-p", "Choose a mark")
"-c", "FF7518", "-p", "Choose mark")
chooseIn, _ := chooseCmd.StdinPipe()
chooseOut, _ := chooseCmd.StdoutPipe()

Expand Down
37 changes: 37 additions & 0 deletions cleancmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"fmt"
"os"

// "strings"

log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func init() {
rootCmd.AddCommand(cleanMarksCmd)
}

// markCmd represents the mark command
var cleanMarksCmd = &cobra.Command{
Use: "clean",
Aliases: []string{"purge"},
Short: "Remove stale marks",
Long: `Remove marks that are stale and do not point at any windows`,
Run: func(_ *cobra.Command, _ []string) {
cleanMarks()
},
}

func cleanMarks() {
log.Printf("Listing marks under %v", stateHome)
_, staleMarks := findMarks()
for _, v := range staleMarks {
fmt.Printf("rm '%v'\n", v)
if err := os.Remove(v); err != nil {
log.Warningf("Failure removing mark file: %v, %v", v, err)
}
}
}
1 change: 1 addition & 0 deletions jq.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
func jq(query string, input interface{}) (interface{}, error) {
var data map[string]interface{}
if err := json.Unmarshal([]byte(input.(string)), &data); err != nil {
log.Printf("Error marshalling JSON: %v", err)
return nil, err
}

Expand Down
72 changes: 59 additions & 13 deletions listcmd.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
package main

import (
"encoding/json"
"fmt"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"os"
"path/filepath"
"strings"

// "strings"

"github.com/MakeNowJust/heredoc"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)

func init() {
Expand All @@ -14,17 +20,28 @@ func init() {

// markCmd represents the mark command
var listMarksCmd = &cobra.Command{
Use: "ls",
Short: "List all the marked windows",
Long: `List all marked windows, displaying their mark as well as the extended titles`,
Run: func(cmd *cobra.Command, args []string) {
log.Printf("Listing marks under %v", stateHome)
Use: "ls",
Aliases: []string{"list"},
Short: "List marked windows",
Long: `List all marked windows, displaying their mark with the window title and app`,
Run: func(_ *cobra.Command, _ []string) {
listMarks()
},
}

func listMarks() {
log.Printf("Listing marks under %v", stateHome)
currentMarks, _ := findMarks()
for _, v := range currentMarks {
fmt.Printf("%v", v)
}
}

func findMarks() ([]string, []string) {
windowMap := getWindows()
matches, _ := filepath.Glob(fmt.Sprintf("%s/*", stateHome))
lines := []string{}
stale := []string{}
for _, match := range matches {
f, _ := os.Stat(match)
if !f.IsDir() {
Expand All @@ -33,17 +50,46 @@ func listMarks() {
log.Printf("ERROR: could not read file", match)
}

app, err := jq(".app", string(data))
id, err := jq(".id", string(data))
if err != nil {
log.Printf("Could not find .app under %v", data)
log.Printf("Could not find .id under %v", data)
}

title, err := jq(".title", string(data))
if err != nil {
log.Printf("Could not find .title under %v", data)
if obj, ok := windowMap[fmt.Sprint(id)]; ok {
line := fmt.Sprintf("%v ⟶ %v\n",
filepath.Base(match),
strings.Join(obj, " | "))
lines = append(lines, line)
} else {
log.Printf("id %v not in windowMap", id)
stale = append(stale, match)
}
}
}
return lines, stale
}

func getWindows() map[string][]string {
windowMap := make(map[string][]string)

fmt.Printf("%s -> %s [%s]\n", filepath.Base(match), title, app)
sc := yabaiscript(heredoc.Doc(`yabai -m query --windows`))
if _, stdout, stderr, err := sc.Exec(); err == nil {

var data []interface{}
if err := json.Unmarshal([]byte(stdout.String()), &data); err != nil {
log.Fatalf("Error marshalling JSON: %v", err)
}

for _, v := range data {
winMap := v.(map[string]interface{})
id := fmt.Sprint(winMap["id"])
title := winMap["title"].(string)
app := winMap["app"].(string)
windowMap[id] = []string{title, id, app}
}
} else {
log.Fatalf("Error running yabai: err: %s, stderr: %s", err, stderr.String())
}

return windowMap
}
2 changes: 1 addition & 1 deletion markcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func init() {
// markCmd represents the mark command
var markCmd = &cobra.Command{
Use: "mark",
Short: "Mark a given window with a letter or number",
Short: "Mark the active window with a letter/number",
Long: `Windows can be marked and assigned letters or numbers as
shortcuts that can later be used in activating/showing those windows`,
Run: func(_ *cobra.Command, args []string) {
Expand Down
3 changes: 3 additions & 0 deletions rootcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,7 @@ func initConfig() {
if viper.GetBool("debug") {
log.SetLevel(log.DebugLevel)
}

// Aliases
viper.RegisterAlias("list", "ls")
}
7 changes: 2 additions & 5 deletions summoncmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ func init() {
// summonCmd represents the summon command
var summonCmd = &cobra.Command{
Use: "summon",
Short: "Summon a given window by its mark",
Short: "Summon a window by mark",
Long: `Summon a given window by its mark`,
Run: func(_ *cobra.Command, args []string) {
var mark string
Expand All @@ -40,12 +40,9 @@ func (w *WinMarker) SummonMark(mark string) {
log.Printf("SummonMark called: %+v, %+v", mark, markFile)

if _, err := os.Stat(markFile); errors.Is(err, os.ErrNotExist) {
n := osascript(fmt.Sprintf((heredoc.Doc(`
osascript(fmt.Sprintf((heredoc.Doc(`
display alert "No mark defined for '%s'" giving up after 1.5
`)), mark))
if _, _, _, err := n.Exec(); err != nil {
log.Printf("Failed invoking notification, %v", err)
}
log.Fatalf("Mark file does not exist: %v (%v)", mark, markFile)
}

Expand Down
4 changes: 2 additions & 2 deletions versioncmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ func init() {
// versionCmd represents the version command
var versionCmd = &cobra.Command{
Use: "version",
Short: "Print the version information",
Long: `Print the version information`,
Short: "Print version information",
Long: `Print the version and build information`,
Run: func(_ *cobra.Command, _ []string) {
fmt.Printf(heredoc.Doc(`
%s version %s
Expand Down

0 comments on commit f60e5ce

Please sign in to comment.