├── .github └── workflows │ ├── push.yml │ └── release.yml ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── main.go ├── proc └── proc.go ├── program └── program.go └── session └── session.go /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | 5 | build: 6 | name: Build 7 | runs-on: ubuntu-latest 8 | steps: 9 | 10 | - name: Set up Go 1.13 11 | uses: actions/setup-go@v1 12 | with: 13 | go-version: 1.13 14 | id: go 15 | 16 | - name: Check out code into the Go module directory 17 | uses: actions/checkout@v1 18 | 19 | - name: Get dependencies 20 | run: | 21 | go get -v -t -d ./... 22 | 23 | - name: Build 24 | run: go build -v . 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: release 2 | name: Release 3 | jobs: 4 | 5 | release-linux-386: 6 | name: release linux/386 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@master 10 | - name: compile and release 11 | uses: ngs/go-release.action@v1.0.1 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | GOARCH: "386" 15 | GOOS: linux 16 | 17 | release-linux-amd64: 18 | name: release linux/amd64 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@master 22 | - name: compile and release 23 | uses: ngs/go-release.action@v1.0.1 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | GOARCH: amd64 27 | GOOS: linux 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rafael Gumieri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sway-session 2 | 3 | Tool for saving the state of the [Sway WM](https://swaywm.org) session and restoring it. 4 | 5 | **At the moment, it is a [PoC](https://en.wikipedia.org/wiki/Proof_of_concept)!** 6 | 7 | [![Actions Status](https://github.com/gumieri/sway-session/workflows/Go/badge.svg)](https://github.com/gumieri/sway-session/actions) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/gumieri/note)](https://goreportcard.com/report/github.com/gumieri/note) 9 | [![GoDoc](https://godoc.org/github.com/gumieri/sway-session?status.svg)](https://godoc.org/github.com/gumieri/sway-session) 10 | 11 | ## Usage 12 | For saving the running programs and its workspace disposition run the given command: 13 | ```bash 14 | sway-session save 15 | ``` 16 | It will create a json file at `$XDG_DATA_HOME/sway-session/sessions/`. 17 | 18 | To restore simply use: 19 | ```bash 20 | sway-session restore 21 | ``` 22 | The recomendation would be to place at the sway config file something like that: 23 | ```config 24 | exec sway-session restore 25 | ``` 26 | 27 | There is a command for constantly save your session: 28 | ```bash 29 | sway-session save-loop 120 30 | ``` 31 | It will save your session every 2 minutes, informed in seconds. If the seconds are not informed it will assume the default value, which is 1 minute. 32 | 33 | The recomendation way to use it would be something like: 34 | ```config 35 | exec sway-session restore && sway-session save-loop 36 | ``` 37 | 38 | ## Supported programs 39 | Considering that a lot of programs have different ways of retrieving it state and restoring it to the desired state, 40 | the `sway-session` can only offer a generic approach for all the ecosystem and for more specific programs (like terminal-emulators) 41 | to offer some rules with more capabilities. 42 | 43 | ### → [alacritty](https://github.com/jwilm/alacritty) 44 | * current working directory 45 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gumieri/sway-session 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/adrg/xdg v0.1.0 7 | github.com/gumieri/go-sway v0.0.0-20190326020239-f26672ca393d 8 | github.com/gumieri/typist v1.3.1 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= 2 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 3 | github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e h1:4ZrkT/RzpnROylmoQL57iVUL57wGKTR5O6KpVnbm2tA= 4 | github.com/BurntSushi/xgbutil v0.0.0-20160919175755-f7c97cef3b4e/go.mod h1:uw9h2sd4WWHOPdJ13MQpwK5qYWKYDumDqxWWIknEQ+k= 5 | github.com/adrg/xdg v0.1.0 h1:xxaf0nLoJXBclKu4H6fSIyDltMl+7K23k/PnrNsKwd0= 6 | github.com/adrg/xdg v0.1.0/go.mod h1:ZuOshBmzV4Ta+s23hdfFZnBsdzmoR3US0d7ErpqSbTQ= 7 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 8 | github.com/gumieri/go-sway v0.0.0-20190326020239-f26672ca393d h1:FKz0Jetl2lSTM608LDapa5O1DzIV+3U4+ofJoszG60E= 9 | github.com/gumieri/go-sway v0.0.0-20190326020239-f26672ca393d/go.mod h1:ojSUJw/6cJnJnTYobE2LbtUGRQa7v7MeT7HO5NVugG4= 10 | github.com/gumieri/typist v0.1.0 h1:ip4B0+bf3cFDXmvZqrXjPksaAJ1ZCzfabtY3Hrk7DVo= 11 | github.com/gumieri/typist v0.1.0/go.mod h1:WcfG796Sv8a7K6ZbIxDIpHOll5cLUzGWx0l/Mlcg9cQ= 12 | github.com/gumieri/typist v1.3.1 h1:UyfPHoPHEzpQolh4Ixxy0BCV9DsvpylOk/9FUFlC6mQ= 13 | github.com/gumieri/typist v1.3.1/go.mod h1:WcfG796Sv8a7K6ZbIxDIpHOll5cLUzGWx0l/Mlcg9cQ= 14 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 15 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strconv" 6 | "time" 7 | 8 | "github.com/gumieri/sway-session/session" 9 | "github.com/gumieri/typist" 10 | ) 11 | 12 | var t = typist.New(&typist.Config{}) 13 | 14 | func main() { 15 | if len(os.Args) <= 1 { 16 | return 17 | } 18 | 19 | switch os.Args[1] { 20 | case "save": 21 | s, err := session.New() 22 | t.Must(err) 23 | 24 | t.Must(s.Save()) 25 | 26 | case "save-loop": 27 | durationInterval := time.Duration(60) 28 | if len(os.Args) >= 3 { 29 | interval, err := strconv.Atoi(os.Args[2]) 30 | t.Must(err) 31 | durationInterval = time.Duration(interval) 32 | } 33 | 34 | for { 35 | s, err := session.New() 36 | t.Must(err) 37 | time.Sleep(durationInterval * time.Second) 38 | t.Must(s.Save()) 39 | session.CleanUp() 40 | } 41 | 42 | case "restore": 43 | s, err := session.LoadNewest() 44 | t.Must(err) 45 | 46 | t.Must(s.Restore()) 47 | 48 | case "clean-up": 49 | t.Must(session.CleanUp()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /proc/proc.go: -------------------------------------------------------------------------------- 1 | package proc 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Procs is a group of processes 11 | type Procs []*Proc 12 | 13 | // Proc maps the data about a system process 14 | type Proc struct { 15 | PID int 16 | PPID int 17 | EXE string 18 | CWD string 19 | CMDLine []string 20 | Stat []string 21 | } 22 | 23 | // NewProc gather the system process data from a informed PID 24 | func NewProc(pid int) (p *Proc, err error) { 25 | p = &Proc{PID: pid} 26 | 27 | pidS := strconv.Itoa(pid) 28 | procDir := "/proc/" + pidS + "/" 29 | 30 | p.EXE, err = os.Readlink(procDir + "exe") 31 | if err != nil { 32 | return 33 | } 34 | 35 | p.CWD, err = os.Readlink(procDir + "cwd") 36 | if err != nil { 37 | return 38 | } 39 | 40 | cmdline, err := ioutil.ReadFile(procDir + "cmdline") 41 | if err != nil { 42 | return 43 | } 44 | p.CMDLine = strings.Split(string(cmdline), "\x00") 45 | 46 | stat, err := ioutil.ReadFile(procDir + "stat") 47 | if err != nil { 48 | return 49 | } 50 | p.Stat = strings.Split(string(stat), " ") 51 | 52 | p.PPID, _ = strconv.Atoi(p.Stat[3]) 53 | 54 | return 55 | } 56 | 57 | // AllProcs gather all running procces at the system to return a collection of it 58 | func AllProcs() *Procs { 59 | files, err := ioutil.ReadDir("/proc/") 60 | if err != nil { 61 | return nil 62 | } 63 | 64 | var procs Procs 65 | for _, f := range files { 66 | pid, err := strconv.Atoi(f.Name()) 67 | if err != nil { 68 | continue 69 | } 70 | 71 | proc, err := NewProc(pid) 72 | if err != nil { 73 | continue 74 | } 75 | 76 | procs = append(procs, proc) 77 | } 78 | 79 | return &procs 80 | } 81 | 82 | // Find a informed PID in the collection of Procs 83 | func (ps *Procs) Find(pid int) *Proc { 84 | for _, p := range *ps { 85 | if p.PID == pid { 86 | return p 87 | } 88 | } 89 | 90 | return nil 91 | } 92 | 93 | // ChildrenOf look for all process having the informed Proc as parent 94 | func (ps *Procs) ChildrenOf(pp *Proc) *Procs { 95 | var c Procs 96 | for _, p := range *ps { 97 | if p.PPID == pp.PID { 98 | c = append(c, p) 99 | } 100 | } 101 | 102 | return &c 103 | } 104 | -------------------------------------------------------------------------------- /program/program.go: -------------------------------------------------------------------------------- 1 | package program 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | 7 | "github.com/gumieri/go-sway" 8 | "github.com/gumieri/sway-session/proc" 9 | ) 10 | 11 | // Program define the information to recreate a program 12 | type Program struct { 13 | Name string `json:"name"` 14 | Workspace string `json:"workspace"` 15 | Command []string `json:"command"` 16 | } 17 | 18 | // NewProgramInput is only used as parameter data to NewProgram 19 | type NewProgramInput struct { 20 | Node *sway.Node 21 | Workspace string 22 | Procs *proc.Procs 23 | } 24 | 25 | // NewProgram create a Program with the definitions of informed Sway Node, for a defined Workspace 26 | func NewProgram(input *NewProgramInput) (p *Program, err error) { 27 | p = &Program{Workspace: input.Workspace} 28 | 29 | proc := input.Procs.Find(input.Node.PID) 30 | if proc == nil { 31 | err = errors.New("PID not found") 32 | return 33 | } 34 | 35 | exeA := strings.Split(proc.EXE, "/") 36 | p.Name = exeA[len(exeA)-1] 37 | 38 | p.Command = proc.CMDLine 39 | switch p.Name { 40 | case "alacritty": 41 | children := *input.Procs.ChildrenOf(proc) 42 | p.Command = []string{proc.CMDLine[0], "--working-directory " + children[0].CWD} 43 | } 44 | 45 | return 46 | } 47 | 48 | // Restore outputs the command for restoring a Program 49 | func (p *Program) Restore() string { 50 | return "workspace " + p.Workspace + "; exec " + strings.Join(p.Command, " ") 51 | } 52 | 53 | // GetProgramsInput is only used as parameter data to GetPrograms 54 | type GetProgramsInput struct { 55 | Parent *sway.Node 56 | Workspace string 57 | Procs *proc.Procs 58 | } 59 | 60 | // GetPrograms read a Sway Tree for mapping the running programs and return a slice of it 61 | func GetPrograms(input *GetProgramsInput) ([]*Program, error) { 62 | programs := make([]*Program, 0) 63 | 64 | for _, node := range input.Parent.Nodes { 65 | switch node.Type { 66 | case sway.Con: 67 | p, err := NewProgram(&NewProgramInput{ 68 | Node: node, 69 | Workspace: input.Workspace, 70 | Procs: input.Procs, 71 | }) 72 | 73 | if err != nil { 74 | return programs, err 75 | } 76 | 77 | programs = append(programs, p) 78 | 79 | case sway.WorkspaceNode: 80 | input.Workspace = node.Name 81 | fallthrough 82 | 83 | default: 84 | nodePrograms, err := GetPrograms(&GetProgramsInput{ 85 | Parent: node, 86 | Workspace: input.Workspace, 87 | Procs: input.Procs, 88 | }) 89 | 90 | if err != nil { 91 | return programs, err 92 | } 93 | 94 | programs = append(programs, nodePrograms...) 95 | } 96 | } 97 | 98 | return programs, nil 99 | } 100 | -------------------------------------------------------------------------------- /session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "sort" 12 | "strconv" 13 | "time" 14 | 15 | "github.com/adrg/xdg" 16 | "github.com/gumieri/go-sway" 17 | "github.com/gumieri/sway-session/proc" 18 | "github.com/gumieri/sway-session/program" 19 | ) 20 | 21 | var configPath = path.Join(xdg.DataHome, "sway-session") 22 | var sessionsPath = path.Join(configPath, "sessions") 23 | 24 | // Session have information about a Sway Session 25 | type Session struct { 26 | FilePath string `json:"string"` 27 | Programs []*program.Program `json:"programs"` 28 | } 29 | 30 | // New create a instance of Session 31 | func New() (s *Session, err error) { 32 | filename := strconv.FormatInt(time.Now().Unix(), 10) + ".json" 33 | filePath := path.Join(sessionsPath, filename) 34 | s = &Session{FilePath: filePath} 35 | 36 | tree, err := sway.GetTree() 37 | if err != nil { 38 | return 39 | } 40 | 41 | s.Programs, err = program.GetPrograms(&program.GetProgramsInput{ 42 | Parent: tree.Root, 43 | Procs: proc.AllProcs(), 44 | }) 45 | 46 | return 47 | } 48 | 49 | func timestampFromFilename(filename string) int { 50 | s := filename[0 : len(filename)-len(filepath.Ext(filename))] 51 | t, err := strconv.Atoi(s) 52 | if err != nil { 53 | return 0 54 | } 55 | 56 | return t 57 | } 58 | 59 | // LoadNewest read the newest saved Session data from the disk 60 | func LoadNewest() (s *Session, err error) { 61 | files, err := ioutil.ReadDir(sessionsPath) 62 | if err != nil { 63 | log.Fatal(err) 64 | } 65 | 66 | if len(files) == 0 { 67 | err = errors.New("No session file found") 68 | return 69 | } 70 | 71 | sort.Slice(files, func(i, j int) bool { 72 | iUnix := timestampFromFilename(files[i].Name()) 73 | jUnix := timestampFromFilename(files[j].Name()) 74 | return iUnix > jUnix 75 | }) 76 | 77 | filePath := path.Join(sessionsPath, files[0].Name()) 78 | b, err := ioutil.ReadFile(filePath) 79 | if err != nil { 80 | return 81 | } 82 | 83 | err = json.Unmarshal(b, &s) 84 | 85 | return 86 | } 87 | 88 | // Save write to disk the Session data 89 | func (s *Session) Save() (err error) { 90 | b, err := json.Marshal(s) 91 | if err != nil { 92 | return 93 | } 94 | 95 | err = ioutil.WriteFile(s.FilePath, b, 0600) 96 | 97 | return 98 | } 99 | 100 | // Restore execute the commands at Sway to restore its state 101 | func (s *Session) Restore() (err error) { 102 | for _, p := range s.Programs { 103 | _, err = sway.RunCommand(p.Restore()) 104 | if err != nil { 105 | return 106 | } 107 | } 108 | 109 | return 110 | } 111 | 112 | // CleanUp delete all session files except the last 113 | func CleanUp() (err error) { 114 | files, err := ioutil.ReadDir(sessionsPath) 115 | if err != nil { 116 | log.Fatal(err) 117 | } 118 | 119 | if len(files) == 0 { 120 | return 121 | } 122 | 123 | sort.Slice(files, func(i, j int) bool { 124 | iUnix := timestampFromFilename(files[i].Name()) 125 | jUnix := timestampFromFilename(files[j].Name()) 126 | return iUnix > jUnix 127 | }) 128 | 129 | for i, file := range files { 130 | if i == 0 { 131 | continue 132 | } 133 | 134 | err = os.Remove(path.Join(sessionsPath, file.Name())) 135 | if err != nil { 136 | return 137 | } 138 | } 139 | 140 | return 141 | } 142 | --------------------------------------------------------------------------------