@@ -10,6 +10,7 @@ import (
1010 "strconv"
1111 "strings"
1212 "sync"
13+ "text/template"
1314 "time"
1415
1516 securejoin "github.com/cyphar/filepath-securejoin"
@@ -34,11 +35,23 @@ import (
3435 "github.com/qubesome/cli/pkg/inception"
3536 "golang.org/x/sys/execabs"
3637 "golang.org/x/term"
38+ "gopkg.in/yaml.v3"
3739)
3840
3941var (
4042 ContainerNameFormat = "qubesome-%s"
4143 defaultProfileImage = "ghcr.io/qubesome/xorg:latest"
44+
45+ appTemplate = `[Desktop Entry]
46+ Version=1.0
47+ Name={{.Name}}
48+ Exec=/bin/sh -c "/usr/local/bin/qubesome run {{.Name}} %U"
49+ Icon=qubesome-generic
50+ StartupNotify=true
51+ Terminal=false
52+ Type=Application
53+ `
54+ qubesomeIconBlack = `<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.dev/svgjs" width="2000" height="1250" viewBox="0 0 2000 1250"><g transform="matrix(1,0,0,1,0,0)"><svg viewBox="0 0 512 320" data-background-color="#ffffff" preserveAspectRatio="xMidYMid meet" height="1250" width="2000" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="tight-bounds" transform="matrix(1,0,0,1,0,0)"><svg viewBox="0 0 512 320" height="320" width="512"><g><svg></svg></g><g><svg viewBox="0 0 512 320" height="320" width="512"><g><path transform="translate(256,160) scale(226.27417,226.27417)" d="M-0.707 0.707l0-1.414 1.414 0 0 1.414z" fill="#000000" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal" data-fill-palette-color="tertiary"></path></g><g transform="matrix(1,0,0,1,140.8,131.0465864758607)"><svg viewBox="0 0 230.4 57.90682704827861" height="57.90682704827861" width="230.4"><g><svg viewBox="0 0 313.6537803795311 78.83114241960186" height="57.90682704827861" width="230.4"><g transform="matrix(1,0,0,1,83.25378037953108,10.894897396630942)"><svg viewBox="0 0 230.40000000000003 57.04134762633998" height="57.04134762633998" width="230.40000000000003"><g id="textblocktransform"><svg viewBox="0 0 230.40000000000003 57.04134762633998" height="57.04134762633998" width="230.40000000000003" id="textblock"><g><svg viewBox="0 0 230.40000000000003 57.04134762633998" height="57.04134762633998" width="230.40000000000003"><g transform="matrix(1,0,0,1,0,0)"><svg width="230.40000000000003" viewBox="1.7 -37.65 195.88000000000002 48.5" height="57.04134762633998" data-palette-color="#ffffff"><path d="M16.6-26.45L21.95-26.45 21.95 10.85 15.8 10.15 15.8-2.65Q13.7 0.75 10.1 0.75L10.1 0.75Q6.1 0.75 3.9-2.9 1.7-6.55 1.7-13.2L1.7-13.2Q1.7-17.45 2.83-20.6 3.95-23.75 5.95-25.45 7.95-27.15 10.6-27.15L10.6-27.15Q14.05-27.15 16.3-23.95L16.3-23.95 16.6-26.45ZM11.85-3.9Q13.05-3.9 14-4.7 14.95-5.5 15.8-7.1L15.8-7.1 15.8-19.8Q14.1-22.55 12.1-22.55L12.1-22.55Q10.3-22.55 9.2-20.33 8.1-18.1 8.1-13.25L8.1-13.25Q8.1-8.1 9.08-6 10.05-3.9 11.85-3.9L11.85-3.9ZM45.8-26.45L45.8 0 40.45 0 40.15-3.35Q38.9-1.3 37.35-0.28 35.8 0.75 33.75 0.75L33.75 0.75Q30.85 0.75 29.17-1.3 27.5-3.35 27.5-6.65L27.5-6.65 27.5-26.45 33.65-26.45 33.65-7Q33.65-3.9 35.75-3.9L35.75-3.9Q36.95-3.9 37.9-4.83 38.85-5.75 39.65-7.45L39.65-7.45 39.65-26.45 45.8-26.45ZM63.65-27.15Q67.5-27.15 69.55-23.65 71.59-20.15 71.59-13.25L71.59-13.25Q71.59-6.75 69.3-3 67 0.75 63 0.75L63 0.75Q61.25 0.75 59.8-0.13 58.35-1 57.3-2.55L57.3-2.55 56.95 0 51.5 0 51.5-37 57.65-37.65 57.65-23.6Q58.7-25.3 60.22-26.23 61.75-27.15 63.65-27.15L63.65-27.15ZM61.35-3.9Q63.2-3.9 64.22-6 65.25-8.1 65.25-13.25L65.25-13.25Q65.25-18.6 64.32-20.58 63.4-22.55 61.7-22.55L61.7-22.55Q60.55-22.55 59.5-21.6 58.45-20.65 57.65-19.1L57.65-19.1 57.65-6.7Q58.3-5.45 59.27-4.67 60.25-3.9 61.35-3.9L61.35-3.9ZM95.04-13.85Q95.04-13.4 94.89-11.3L94.89-11.3 81.24-11.3Q81.44-7.2 82.67-5.58 83.89-3.95 86.14-3.95L86.14-3.95Q87.69-3.95 88.94-4.48 90.19-5 91.64-6.15L91.64-6.15 94.19-2.65Q90.69 0.75 85.79 0.75L85.79 0.75Q80.59 0.75 77.82-2.85 75.04-6.45 75.04-13L75.04-13Q75.04-19.55 77.69-23.35 80.34-27.15 85.19-27.15L85.19-27.15Q89.84-27.15 92.44-23.78 95.04-20.4 95.04-13.85L95.04-13.85ZM89.04-15.3L89.04-15.65Q89.04-19.4 88.14-21.13 87.24-22.85 85.24-22.85L85.24-22.85Q83.39-22.85 82.42-21.18 81.44-19.5 81.24-15.3L81.24-15.3 89.04-15.3ZM106.69-27.15Q111.39-27.15 114.64-24.05L114.64-24.05 112.29-20.7Q110.89-21.7 109.64-22.2 108.39-22.7 107.09-22.7L107.09-22.7Q105.64-22.7 104.82-21.93 103.99-21.15 103.99-19.8L103.99-19.8Q103.99-18.45 104.92-17.63 105.84-16.8 108.64-15.7L108.64-15.7Q112.09-14.35 113.74-12.5 115.39-10.65 115.39-7.55L115.39-7.55Q115.39-3.7 112.69-1.48 109.99 0.75 105.84 0.75L105.84 0.75Q103.14 0.75 100.87-0.23 98.59-1.2 96.89-2.95L96.89-2.95 99.89-6.25Q102.69-3.8 105.59-3.8L105.59-3.8Q107.24-3.8 108.19-4.67 109.14-5.55 109.14-7.1L109.14-7.1Q109.14-8.25 108.74-8.97 108.34-9.7 107.34-10.33 106.34-10.95 104.34-11.7L104.34-11.7Q100.89-13.05 99.42-14.85 97.94-16.65 97.94-19.45L97.94-19.45Q97.94-22.8 100.32-24.98 102.69-27.15 106.69-27.15L106.69-27.15ZM128.64-27.15Q133.64-27.15 136.41-23.65 139.19-20.15 139.19-13.25L139.19-13.25Q139.19-6.65 136.39-2.95 133.59 0.75 128.64 0.75L128.64 0.75Q123.69 0.75 120.89-2.88 118.09-6.5 118.09-13.25L118.09-13.25Q118.09-19.95 120.89-23.55 123.69-27.15 128.64-27.15L128.64-27.15ZM128.64-22.5Q126.49-22.5 125.49-20.38 124.49-18.25 124.49-13.25L124.49-13.25Q124.49-8.2 125.49-6.08 126.49-3.95 128.64-3.95L128.64-3.95Q130.79-3.95 131.79-6.08 132.79-8.2 132.79-13.25L132.79-13.25Q132.79-18.3 131.79-20.4 130.79-22.5 128.64-22.5L128.64-22.5ZM167.39-27.15Q170.04-27.15 171.64-25.1 173.24-23.05 173.24-19.6L173.24-19.6 173.24 0 167.19 0 167.19-18.95Q167.19-20.85 166.69-21.68 166.19-22.5 165.24-22.5L165.24-22.5Q163.29-22.5 161.49-18.8L161.49-18.8 161.49 0 155.49 0 155.49-18.95Q155.49-22.5 153.59-22.5L153.59-22.5Q151.54-22.5 149.79-18.8L149.79-18.8 149.79 0 143.74 0 143.74-26.45 149.09-26.45 149.49-23.05Q152.09-27.15 155.74-27.15L155.74-27.15Q157.59-27.15 158.94-26 160.29-24.85 160.99-22.75L160.99-22.75Q163.64-27.15 167.39-27.15L167.39-27.15ZM197.58-13.85Q197.58-13.4 197.43-11.3L197.43-11.3 183.78-11.3Q183.98-7.2 185.21-5.58 186.43-3.95 188.68-3.95L188.68-3.95Q190.23-3.95 191.48-4.48 192.73-5 194.18-6.15L194.18-6.15 196.73-2.65Q193.23 0.75 188.33 0.75L188.33 0.75Q183.13 0.75 180.36-2.85 177.58-6.45 177.58-13L177.58-13Q177.58-19.55 180.23-23.35 182.88-27.15 187.73-27.15L187.73-27.15Q192.38-27.15 194.98-23.78 197.58-20.4 197.58-13.85L197.58-13.85ZM191.58-15.3L191.58-15.65Q191.58-19.4 190.68-21.13 189.78-22.85 187.78-22.85L187.78-22.85Q185.93-22.85 184.96-21.18 183.98-19.5 183.78-15.3L183.78-15.3 191.58-15.3Z" opacity="1" transform="matrix(1,0,0,1,0,0)" fill="#ffffff" class="wordmark-text-0" data-fill-palette-color="quaternary" id="text-0"></path></svg></g></svg></g></svg></g></svg></g><g><svg viewBox="0 0 69.78768719729524 78.83114241960186" height="78.83114241960186" width="69.78768719729524"><g><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" version="1.1" style="shape-rendering:geometricPrecision;text-rendering:geometricPrecision;image-rendering:optimizeQuality;" viewBox="19 15.25 61 68.90470053837926" x="0" y="0" fill-rule="evenodd" clip-rule="evenodd" height="78.83114241960186" width="69.78768719729524" class="icon-icon-0" data-fill-palette-color="quaternary" id="icon-0"><g fill="#ffffff" data-fill-palette-color="quaternary"><path class="" d="M53 16C62 21 69 25 77 30 79 31 80 32 80 35V64C80 66 79 68 77 69 68 74 62 78 53 83 50 84 49 85 46 83 37 78 30 74 22 69 20 68 19 67 19 64V35C19 33 20 32 22 30 31 25 38 21 47 16 49 15 51 15 53 16M52 51V78C60 73 68 69 75 64V37zM48 78V51L25 37V64C33 69 41 73 48 78M27 34L50 48 73 34C65 29 57 25 50 20 42 25 34 29 27 34" fill="#ffffff" fill-rule="nonzero" data-fill-palette-color="quaternary"></path></g></svg></g></svg></g></svg></g></svg></g></svg></g><defs></defs></svg><rect width="512" height="320" fill="none" stroke="none" visibility="hidden"></rect></g></svg></g></svg>`
4255)
4356
4457func Run (opts ... command.Option [Options ]) error {
@@ -321,7 +334,7 @@ func Start(runner string, profile *types.Profile, cfg *types.Config, interactive
321334
322335 err = createNewDisplay (binary ,
323336 creds .CA , creds .ClientPEM , creds .ClientKeyPEM ,
324- profile , strconv .Itoa (int (profile .Display )), interactive )
337+ profile , strconv .Itoa (int (profile .Display )), interactive , cfg )
325338 if err != nil {
326339 return err
327340 }
@@ -425,7 +438,7 @@ func startWindowManager(bin, name, display, wm string) error {
425438 return nil
426439}
427440
428- func createNewDisplay (bin string , ca , cert , key []byte , profile * types.Profile , display string , interactive bool ) error {
441+ func createNewDisplay (bin string , ca , cert , key []byte , profile * types.Profile , display string , interactive bool , cfg * types. Config ) error {
429442 command := "Xephyr"
430443 res , err := resolution .Primary ()
431444 if err != nil {
@@ -603,7 +616,7 @@ func createNewDisplay(bin string, ca, cert, key []byte, profile *types.Profile,
603616 return err
604617 }
605618
606- err = setupAppsDir (profile )
619+ err = setupAppsDir (profile , cfg )
607620 if err != nil {
608621 return err
609622 }
@@ -726,17 +739,31 @@ func setupRunUserDir(dir string) error {
726739 return nil
727740}
728741
729- func setupAppsDir (profile * types.Profile ) error {
742+ func setupAppsDir (profile * types.Profile , cfg * types. Config ) error {
730743 dir := files .ProfileDir (profile .Name )
731- err := os .MkdirAll (filepath .Join (dir , "applications" ), files .DirMode )
744+
745+ appsDir := filepath .Join (dir , "applications" )
746+ err := os .MkdirAll (appsDir , files .DirMode )
732747 if err != nil {
733748 return fmt .Errorf ("failed to create <profile>/applications dir: %w" , err )
734749 }
735- err = os .MkdirAll (filepath .Join (dir , "icons" ), files .DirMode )
750+
751+ appsRoot , err := os .OpenRoot (appsDir )
752+ if err != nil {
753+ return err
754+ }
755+
756+ iconsDir := filepath .Join (dir , "icons" )
757+ err = os .MkdirAll (iconsDir , files .DirMode )
736758 if err != nil {
737759 return fmt .Errorf ("failed to create <profile>/icons dir: %w" , err )
738760 }
739761
762+ iconsRoot , err := os .OpenRoot (iconsDir )
763+ if err != nil {
764+ return err
765+ }
766+
740767 for _ , name := range profile .Flatpaks {
741768 src := filepath .Join (files .FlatpakApps (), name + ".desktop" )
742769 target := filepath .Join (dir , "applications" , name + ".desktop" )
@@ -756,6 +783,87 @@ func setupAppsDir(profile *types.Profile) error {
756783
757784 slog .Debug ("added flatpak workload" , "name" , name )
758785 }
786+
787+ return hydrateApps (cfg , appsRoot , iconsRoot )
788+ }
789+
790+ func createGenericIcon (root * os.Root ) error {
791+ f , err := root .Create ("qubesome-generic.svg" )
792+ if err != nil {
793+ return err
794+ }
795+ defer f .Close ()
796+
797+ _ , err = f .WriteString (qubesomeIconBlack )
798+ return err
799+ }
800+
801+ func hydrateApps (cfg * types.Config , appsRoot , iconsRoot * os.Root ) error {
802+ slog .Debug ("hydrating profile workloads" )
803+ wf , err := cfg .WorkloadFiles ()
804+ if err != nil {
805+ return fmt .Errorf ("cannot get workloads files: %w" , err )
806+ }
807+
808+ if len (wf ) == 0 {
809+ return nil
810+ }
811+
812+ if err = createGenericIcon (iconsRoot ); err != nil {
813+ slog .Debug ("failed to create generic icon" , "error" , err )
814+ }
815+
816+ for _ , fn := range wf {
817+ slog .Debug ("profile workload" , "filename" , fn )
818+ fi , err := os .Stat (fn )
819+ if err != nil {
820+ slog .Error ("cannot stat file" , "file" , fn , "error" , err )
821+ }
822+
823+ if ! fi .Mode ().IsRegular () {
824+ continue
825+ }
826+
827+ data , err := os .ReadFile (fn )
828+ if err != nil {
829+ slog .Error ("cannot read file" , "filename" , fn , "error" , err )
830+ continue
831+ }
832+
833+ w := types.Workload {}
834+ err = yaml .Unmarshal (data , & w )
835+ if err != nil {
836+ slog .Error ("cannot unmarshal workload file" , "filename" , fn , "error" , err )
837+ continue
838+ }
839+
840+ w .Name = filepath .Base (strings .TrimSuffix (fn , filepath .Ext (fn )))
841+
842+ if err = w .Validate (); err != nil {
843+ slog .Error ("invalid workload" , "error" , err )
844+ continue
845+ }
846+
847+ appFile := w .Name + ".desktop"
848+ f , err := appsRoot .Create (appFile )
849+ if err != nil {
850+ slog .Error ("cannot create file" , "filename" , appFile , "error" , err )
851+ continue
852+ }
853+
854+ tmpl , err := template .New ("app" ).Parse (appTemplate )
855+ if err != nil {
856+ f .Close ()
857+
858+ slog .Error ("cannot create template" , "error" , err )
859+ continue
860+ }
861+ err = tmpl .Execute (f , w )
862+ if err != nil {
863+ slog .Error ("cannot execute template" , "error" , err )
864+ }
865+ f .Close ()
866+ }
759867 return nil
760868}
761869
0 commit comments