Set-up monitors for sway tiling window manager
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

691 lines
19 KiB

// UI around SwayRandr
package main
import (
"os"
"fmt"
"flag"
"strings"
"strconv"
"os/exec"
. "codeberg.org/snaums/swayrandr/lib"
"github.com/gotk3/gotk3/gtk"
"github.com/gotk3/gotk3/glib"
)
var Version string = ""; // Verion string; filled by Makefile
var BuildDate string = ""; // BuildDate; filled by Makefile
var BuildWith string = ""; // Compiler; filled by Makefile
var GladeString string = "";
var GtkBuilder *gtk.Builder
// structure with references to GTK-elements containing Information
// of a single monitor
type MonitorLine struct {
Frame *gtk.Frame // Frame element
NameLabel *gtk.Label // Name element (containing the connection name, like HDMI-1)
Name string
EnableBox *gtk.CheckButton // enable or disable the monitor
PrimaryBox *gtk.RadioButton
PositionBox *gtk.ComboBoxText // position the monitor (in a cross)
ResolutionBox *gtk.ComboBoxText // select resolution
RefreshBox *gtk.ComboBoxText // select refresh rate
ScaleBox *gtk.SpinButton
}
// structure with references to important objects in
// the main window
type MainWindow struct {
Window *gtk.ApplicationWindow // reference to the Window
ApplyBtn *gtk.Button // reference to the "Apply"-Button
CancelBtn *gtk.Button // reference to the "cancel"-Button
FileBtn *gtk.Button
RefreshBtn *gtk.Button
MonitorLabel *gtk.Label // label on the top with the monitor-count
MainBox *gtk.Box // main-content box; will be filled with the monitor lines
HdrBar *gtk.HeaderBar
Monitors []MonitorLine // list of lines of GTK-Elements; one for each monitor
RadioButtonGroup *glib.SList
ConfigFile string
}
var wnd *MainWindow
var screens []Screen
// fill in all modes available for a screen
func FillModes ( s Screen, box *gtk.ComboBoxText ) error {
var modelist []string;
for _, m := range s.Modes {
var entry string = strconv.Itoa(m.Width)+"x"+strconv.Itoa(m.Height)
var isIn bool = false
for _, v := range modelist {
if entry == v {
isIn = true
break;
}
}
if isIn == false {
modelist = append ( modelist, entry )
}
}
box.RemoveAll();
var isActive bool = false;
for k, m := range modelist {
box.Append ( strconv.Itoa(k), m )
var w, h int
xy := strings.Split ( m, "x" );
w, _ = strconv.Atoi ( xy[0] );
h, _ = strconv.Atoi ( xy[1] );
if s.CurrentMode.Width == w &&
s.CurrentMode.Height == h {
box.SetActive ( k );
isActive = true;
}
}
if isActive == false {
box.SetActive ( 0 );
}
return nil;
}
// fill the refresh-rate box with its entries;
// depends on the currently selected mode
func FillRefreshRates ( s Screen, resolution string, box *gtk.ComboBoxText ) error {
var w, h int
if resolution == "" {
return nil;
}
xy := strings.Split ( resolution, "x" );
if len(xy) < 2 {
return nil;
}
w, _ = strconv.Atoi ( xy[0] );
h, _ = strconv.Atoi ( xy[1] );
var refreshList []string
for _, m := range s.Modes {
var rl string = strconv.FormatFloat ( float64( (float64(m.Refresh))/1000.0 ), 'f', 2, 64 )
if m.Width == w && m.Height == h {
var IsIn bool = false
for _, k := range refreshList {
if k == rl {
IsIn = true;
}
}
if IsIn == false {
refreshList = append ( refreshList, rl );
}
}
}
box.RemoveAll();
var current string = strconv.FormatFloat ( float64( (float64(s.CurrentMode.Refresh))/1000.0 ), 'f', 2, 64 )
var setactive bool = false;
for k, r := range refreshList {
box.AppendText ( r );
if current == r && w == s.CurrentMode.Width && h == s.CurrentMode.Height {
setactive = true;
box.SetActive ( k );
}
}
if setactive == false {
box.SetActive ( 0 );
}
return nil
}
// prefill and preset the position-box with its entries
func FillPositions ( pos *gtk.ComboBoxText, preset string ) error {
positions := []string{ "left", "right", "above", "below", "middle" };
for k, p := range positions {
pos.AppendText ( p );
if preset == p || (preset == "" && p == "middle" ) {
pos.SetActive ( k );
}
}
return nil;
}
func FillScale ( screen Screen, scale *gtk.SpinButton ) error {
scale.SetValue ( screen.Scale );
return nil;
}
// set sensitivity of the gtk-elements
func SetSensitive ( screen Screen, ml MonitorLine, active bool ) error {
ml.EnableBox.SetActive ( active );
ml.PrimaryBox.SetActive ( active );
ml.PositionBox.SetSensitive ( active );
ml.ResolutionBox.SetSensitive ( active );
ml.RefreshBox.SetSensitive ( active );
ml.ScaleBox.SetSensitive ( active );
return nil;
}
// for a single monitor: create a line of GTK-elements
// to display and alter information of the monitor
func CreateMonitorLine ( screen Screen, mainWnd *MainWindow, PosPreset string ) error {
frame, err := gtk.FrameNew ( screen.Make + " " + screen.Model )
if err != nil {
return err
}
box, err := gtk.BoxNew ( gtk.ORIENTATION_HORIZONTAL, 0 );
if err != nil {
return err
}
nameLabel, err := gtk.LabelNew ( screen.Name );
if err != nil {
return err
}
EnableBox, err := gtk.CheckButtonNewWithLabel ( "Enable" );
if err != nil {
return err
}
PrimaryBox, err := gtk.RadioButtonNewWithLabel ( mainWnd.RadioButtonGroup, "Primary" );
if err != nil {
return err
}
mainWnd.RadioButtonGroup, _ = PrimaryBox.GetGroup();
PosLabel, err := gtk.LabelNew ( "Position" );
if err != nil {
return err
}
PosBox, err := gtk.ComboBoxTextNew ();
if err != nil {
return err
}
ResLabel, err := gtk.LabelNew ("Resolution" );
if err != nil {
return err
}
ResBox, err := gtk.ComboBoxTextNew ();
if err != nil {
return err
}
RefreshLabel, err := gtk.LabelNew ("Refresh-Rate");
if err != nil {
return err
}
RefreshBox, err := gtk.ComboBoxTextNew ()
if err != nil {
return err;
}
ScaleLabel, err := gtk.LabelNew ("Scale");
if err != nil {
return err
}
ScaleBox, err := gtk.SpinButtonNewWithRange ( 0, 50, 0.1)
if err != nil {
return err;
}
box.Add ( nameLabel );
box.Add ( EnableBox );
box.Add ( PrimaryBox );
box.Add ( PosLabel );
box.Add ( PosBox );
box.Add ( ResLabel );
box.Add ( ResBox );
box.Add ( RefreshLabel );
box.Add ( RefreshBox );
box.Add ( ScaleLabel );
box.Add ( ScaleBox );
frame.Add ( box );
frame.SetMarginTop ( 5 );
frame.SetMarginBottom ( 5 );
nameLabel.SetMarginStart ( 5 );
nameLabel.SetMarginEnd ( 10 );
EnableBox.SetMarginEnd ( 10 );
PrimaryBox.SetMarginEnd ( 10 );
PosLabel.SetMarginEnd ( 5 );
PosBox.SetMarginEnd ( 10 );
ResLabel.SetMarginEnd ( 5 );
ResBox.SetMarginEnd ( 10 );
RefreshLabel.SetMarginEnd ( 5 );
RefreshBox.SetMarginEnd ( 10 );
ScaleLabel.SetMarginEnd ( 5 );
ScaleBox.SetMarginEnd ( 5 );
frame.ShowAll();
mainWnd.MainBox.Add ( frame );
nm := MonitorLine{
Name: screen.Name,
Frame: frame,
NameLabel: nameLabel,
EnableBox: EnableBox,
PositionBox: PosBox,
ResolutionBox: ResBox,
RefreshBox: RefreshBox,
ScaleBox: ScaleBox,
PrimaryBox: PrimaryBox,
};
FillPositions ( nm.PositionBox, PosPreset );
FillModes ( screen, nm.ResolutionBox );
FillRefreshRates ( screen, nm.ResolutionBox.GetActiveText(), nm.RefreshBox );
FillScale ( screen, nm.ScaleBox );
SetSensitive ( screen, nm, screen.Active );
PrimaryBox.SetActive ( screen.Primary );
ResBox.Connect ("changed", func () {
FillRefreshRates ( screen, nm.ResolutionBox.GetActiveText(), nm.RefreshBox );
});
EnableBox.Connect ("toggled", func () {
SetSensitive ( screen, nm, nm.EnableBox.GetActive() );
});
wnd.Monitors = append ( wnd.Monitors, nm );
return nil;
}
// for all monitors: create a line with
// resolution, position and refresh-rate
func FillMonitorLines ( screens []Screen, mainWnd *MainWindow ) error {
wnd.MonitorLabel.SetLabel ("Found " + strconv.Itoa ( len(screens) ) + " Monitors");
for _, s := range screens {
err := CreateMonitorLine ( s, mainWnd, "" );
if err != nil {
return err;
}
}
return nil
}
type MonitorConfig struct {
Screen Screen;
Enabled bool;
Primary bool;
Position string;
Resolution string;
Refresh string;
X,Y,Width,Height int;
RefreshValue int;
Scale float64;
ConfigFile string;
}
func Msg ( text string ) {
obj := gtk.MessageDialogNew (
wnd.Window,
gtk.DIALOG_DESTROY_WITH_PARENT,
gtk.MESSAGE_WARNING,
gtk.BUTTONS_OK,
text );
obj.SetDefaultResponse ( gtk.RESPONSE_OK );
obj.Connect ( "response", func () {
obj.Destroy();
});
obj.Run();
}
// Find necessary Objects in the window
// Fill the Window with the Monitor Information
// Set global Object-Signal Handlers
func PrepareMainWindow () *MainWindow {
if wnd != nil {
return wnd;
}
var err error
wnd = &MainWindow{}
obj, _ := GtkBuilder.GetObject ("MainWindow");
wnd.Window = obj.(*gtk.ApplicationWindow)
obj, _ = GtkBuilder.GetObject ( "MainBox" );
wnd.MainBox = obj.(*gtk.Box)
obj, _ = GtkBuilder.GetObject ("ApplyBtn");
wnd.ApplyBtn = obj.(*gtk.Button)
obj, _ = GtkBuilder.GetObject ("CancelBtn");
wnd.CancelBtn = obj.(*gtk.Button)
obj, _ = GtkBuilder.GetObject ("RefreshBtn");
wnd.RefreshBtn = obj.(*gtk.Button)
obj, _ = GtkBuilder.GetObject ("MonitorLabel");
wnd.MonitorLabel = obj.(*gtk.Label)
obj, _ = GtkBuilder.GetObject ("FileBtn");
wnd.FileBtn = obj.(*gtk.Button)
obj, _ = GtkBuilder.GetObject ("HdrBar");
wnd.HdrBar = obj.(*gtk.HeaderBar)
screens, err = ReadScreen ();
if err != nil {
return nil;
}
err = FillMonitorLines ( screens, wnd );
if err != nil {
fmt.Println ("Problems filling the Window: ", err.Error())
}
wnd.Window.Connect ("destroy", func() {
gtk.MainQuit()
});
wnd.CancelBtn.Connect ("clicked", func() {
wnd.Window.Close()
gtk.MainQuit()
});
wnd.RefreshBtn.Connect ("clicked", func() {
ReloadSway();
});
wnd.FileBtn.Connect ("clicked", func () {
fchoose, err := gtk.FileChooserNativeDialogNew (
"Choose the Monitor-Config file",
wnd.Window,
gtk.FILE_CHOOSER_ACTION_SAVE,
"Open",
"Cancel",
);
if err != nil {
Msg ( "Cannot create file chooser: " + err.Error() );
return
}
fchoose.SetSelectMultiple ( false );
ret := fchoose.Run ();
if ret == int(gtk.RESPONSE_ACCEPT) {
wnd.ConfigFile = fchoose.GetFilename();
wnd.HdrBar.SetSubtitle ( wnd.ConfigFile );
}
});
wnd.ApplyBtn.Connect ("clicked", func() {
screens, err := ReadScreen ();
if err != nil {
Msg ("Cannot read screen Information from sway");
return
}
// gather all information from the Window
var mc []MonitorConfig
for _, line := range wnd.Monitors {
var mx MonitorConfig
s := FindScreen ( screens, line.Name );
if s == nil {
Msg ("Cannot find the following screen: "+ line.Name );
return
}
mx.Screen = *s;
mx.Enabled = line.EnableBox.GetActive();
mx.Position = line.PositionBox.GetActiveText();
mx.Resolution = line.ResolutionBox.GetActiveText();
mx.Refresh = line.RefreshBox.GetActiveText();
mx.Scale = line.ScaleBox.GetValue();
mx.ConfigFile = wnd.ConfigFile;
if mx.Scale < 0 || mx.Scale > 50 {
Msg ("Scale out of bounds");
return;
}
mx.X = 0;
mx.Y = 0;
rv, err := strconv.ParseFloat ( mx.Refresh, 64 );
if err != nil {
Msg ("Cannot Parse float: "+mx.Refresh);
return
}
mx.RefreshValue = (int( rv * 1000 ))
xy := strings.Split ( mx.Resolution, "x" );
mx.Width, err = strconv.Atoi ( xy[0] );
if err != nil {
Msg ("Cannot Parse int for Width: "+xy[0]);
return
}
mx.Height, err = strconv.Atoi ( xy[1] );
if err != nil {
Msg ("Cannot Parse int for height: "+xy[1]);
return
}
mc = append( mc, mx );
}
// validate resolution selection
for k, _ := range mc {
m := &mc[k]
var IsIn bool = false;
for _, ms := range m.Screen.Modes {
if m.Width == ms.Width &&
m.Height == ms.Height {
if m.RefreshValue >= ms.Refresh-5 &&
m.RefreshValue <= ms.Refresh+5 {
m.RefreshValue = ms.Refresh;
IsIn = true;
break;
}
}
}
if IsIn != true {
Msg ("Selected resolution is not available on screen (" + m.Screen.Name + ")");
return
}
}
// position the screens
Positioning ( &mc );
// run swayrandr once per monitor
for _, m := range mc {
var args []string = []string{ "--output", m.Screen.Name };
var run bool;
if m.Enabled == false {
args = append ( args, "--disable" );
if m.Screen.Active == true {
run = true;
}
} else {
args = append ( args, "--enable");
if m.Screen.Active == false {
run = true;
}
if m.Width != m.Screen.CurrentMode.Width ||
m.Height != m.Screen.CurrentMode.Height ||
m.RefreshValue != m.Screen.CurrentMode.Refresh {
args = append ( args, "--mode" );
args = append ( args, strconv.Itoa ( m.Width ) + "x" + strconv.Itoa( m.Height ) + "@" + strconv.FormatFloat ( float64(m.RefreshValue)/1000.0, 'f', -1, 64 ) );
run = true;
}
if m.X != m.Screen.Rect.X ||
m.Y != m.Screen.Rect.Y {
args = append ( args, "--position" );
args = append ( args, strconv.Itoa ( m.X ) + "+" + strconv.Itoa ( m.Y ) );
run = true;
}
if m.Scale != m.Screen.Scale {
args = append ( args, "--scale" );
args = append ( args, strconv.FormatFloat ( m.Scale, 'f', -1, 64 ));
run = true;
}
if m.ConfigFile != "" {
args = append ( args, "--config-file" );
args = append ( args, m.ConfigFile );
}
}
if run == true {
//fmt.Println ( args );
info, err := os.Stat ( "swayrandr" );
var program string;
if os.IsNotExist ( err ) || info.Mode().IsRegular () == false {
program = "swayrandr";
} else {
program = "./swayrandr";
}
out, err := exec.Command ( program, args... ).CombinedOutput ();
if err != nil {
Msg ("Cannot run swayrandr: " + err.Error() + "\nOutput: " + string(out) );
}
}
}
});
return wnd;
}
// calculate X-Y Positions of the monitors by their selected
// positionBox-state
func Positioning ( mc *[]MonitorConfig ) {
// find the left-most monitor(s)
var currentX int = 0;
var nextX int = 0;
var adj_width int
var adj_height int
for i := 0; i < len(*mc); i++ {
var m *MonitorConfig = &(*mc)[i];
if m.Position == "left" {
m.X = 0;
adj_width = (m.Width*100/int(m.Scale*100))
if m.Width > nextX {
nextX = adj_width;
}
}
}
// middle monitors
currentX = nextX;
for i := 0; i < len(*mc); i++ {
var m *MonitorConfig = &(*mc)[i];
if m.Position == "middle" || m.Position == "above" || m.Position == "below" {
m.X = currentX;
adj_width = (m.Width*100/int(m.Scale*100))
if currentX + adj_width > nextX {
nextX = currentX + adj_width
}
}
}
// right monitors
currentX = nextX;
for i := 0; i < len(*mc); i++ {
var m *MonitorConfig = &(*mc)[i];
if m.Position == "right" {
m.X = currentX;
}
}
// Position the screens in the Y-Axis
var currentY int = 0;
var nextY int = 0;
for i := 0; i < len(*mc); i++ {
var m *MonitorConfig = &(*mc)[i];
if m.Position == "above" {
m.X = 0;
adj_height = (m.Height*100/int(m.Scale*100));
if m.Height > nextY {
nextY = adj_height
}
}
}
// middle monitors
currentY = nextY;
for i := 0; i < len(*mc); i++ {
var m *MonitorConfig = &(*mc)[i];
if m.Position == "middle" || m.Position == "left" || m.Position == "right" {
m.Y = currentY;
adj_height = (m.Height*100/int(m.Scale*100));
if currentY + m.Height > nextY {
nextY = currentY + adj_height;
}
}
}
// right monitors
currentY = nextY;
for i := 0; i < len(*mc); i++ {
var m *MonitorConfig = &(*mc)[i];
if m.Position == "below" {
m.Y = currentY;
}
}
}
// entry point into the LSwayRandr GUI
// builds the Windows from the glade-file and start the GTK-App
func main () {
var version bool
flag.BoolVar ( &version, "version", false, "Print version information" );
flag.Parse();
if version == true {
fmt.Println ("SwayRandr version:\t", Version );
fmt.Println (" Build on: \t", BuildDate );
fmt.Println (" Build with: \t", BuildWith );
fmt.Println ("Sway version: \t", SwayVersion() );
return;
}
gtk.Init ( nil );
var err error
GtkBuilder, err = gtk.BuilderNew();
if err != nil {
fmt.Println ("Builder error: ", err);
return;
}
info, err := os.Stat ( "ui.glade" );
if os.IsNotExist ( err ) || info.Mode().IsRegular() == false {
err = GtkBuilder.AddFromString ( GladeString );
} else {
// load window specification from glade-file
fmt.Println ("Loading glade description from file");
err = GtkBuilder.AddFromFile ("ui.glade");
}
if err != nil {
fmt.Println ("Builder parsing error: ", err);
return;
}
PrepareMainWindow ();
wnd.Window.Show();
gtk.Main()
}