Skip to content
Snippets Groups Projects
Select Git revision
  • 4d10bda06731faa294a50a798cdec9f84003a8fa
  • master default
2 results

Program.cs

Blame
  • Program.cs 17.93 KiB
    // i3csstatus - Alternative generator of i3 status bar written in c#
    // (c)   2022 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>
    
    // This program is free software: you can redistribute it and/or modify
    // it under the terms of the GNU General Public License as published by
    // the Free Software Foundation, either version 3 of the License, or
    // any later version.
    
    // This program is distributed in the hope that it will be useful,
    // but WITHOUT ANY WARRANTY; without even the implied warranty of
    // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    // GNU General Public License for more details.
    
    // You should have received a copy of the GNU General Public License
    // along with this program.  If not, see <https://www.gnu.org/licenses/>.
    
    #nullable enable
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    using System.IO;
    
    using Pastel;
    using System.Text.Json;
    using System.Text.Json.Nodes;
    using System.Linq;
    using System.Threading;
    using StringInfo = System.Globalization.StringInfo;
    
    using System.CommandLine;
    using System.Runtime.InteropServices;
    
    using Color = System.Drawing.Color;
    using Process = System.Diagnostics.Process;
    
    using Config;
    
    namespace i3csstatus;
    
    class PrintAndExit: Exception
    {
    	public string ToPrint;
    	public int ExitCode;
    	public PrintAndExit(string _ToPrint, int _ExitCode=1)
    	{
    		ToPrint = _ToPrint;
    		ExitCode = _ExitCode;
    	}
    	public override string Message { get => $"Print \"{ToPrint}\" and exit with exit code {ExitCode}"; }
    }
    
    
    static class POSIX
    {
    	[DllImport("libc", SetLastError = true)]
    	public static extern int mkfifo(string path, int mode);
    	[DllImport("libc", CallingConvention=CallingConvention.Cdecl, SetLastError=true)]
    	public static extern int kill(int pid, int sig);
    	public static int SIGUSR1 = 10;
    	public static int EEXIST = 17;
    	public static int ERRNO { get => Marshal.GetLastPInvokeError();}
    	public static void Bash(string cmd)
    	{
    		var process = new Process();
    		process.StartInfo.FileName = "bash";
    		process.StartInfo.Arguments = "";
    		process.StartInfo.RedirectStandardInput = true;
    		process.Start();
    		TextWriter stdin = process.StandardInput;
    		stdin.WriteLine("exec 1> /dev/stderr");
    		stdin.Write(cmd);
    		stdin.Close();
    	}
    }
    
    static class ProcesExtended
    {
    	public static int KillBySignal(this Process p, int sig)
    	{
    		return POSIX.kill(p.Id, sig);
    	}
    }
    
    static class JsonExtended
    {
    	public static JsonObject RemoveNull(this JsonObject o)
    	{
    		List<string> toDelete = new();
    		foreach(var (k, v) in o)
    		{
    			if(v == null)
    				toDelete.Add(k);
    		}
    		foreach(var k in toDelete)
    			o.Remove(k);
    		return o;
    	}
    }
    
    static class ColorExtended
    {
    	static public string ToHex(this Color c)
    	{
    		return $"#{c.R:X2}{c.G:X2}{c.B:X2}";
    	}
    	static public Color ToColor(this string s)
    	{
    		return System.Drawing.ColorTranslator.FromHtml(s);
    	}
    }
    
    enum Align
    {
    	Left,
    	Right,
    	Center
    }
    enum Markup
    {
    	None,
    	Pango
    }
    
    record Coordinates(
    		int X,
    		int Y
    		);
    
    enum MouseButton
    {
    	Left,
    	Right,
    	Middle
    }
    
    [Flags]
    enum Modifiers
    {
    	Shift=1,
    	Control=2,
    	Super=4,
    	Alt=8
    }
    
    record ClickEvent(
    		Coordinates Relative, // relative_x, relative_y
    		Coordinates Size, // width, height
    		MouseButton Button,
    		Coordinates? Absolute = null, // x, y
    		Coordinates? Output = null, // output_x, output_y
    		Modifiers Modifiers = 0
    		);
    
    record Block(
    		string Text,
    		string? ShortText=null,
    		Color? Color=null,
    		Color? BackgroundColor=null,
    		Color? BorderColor=null,
    		int? BorderTop=null,
    		int? BorderRight=null,
    		int? BorderBottom = null,
    		int? BorderLeft = null,
    		string? MinWidth_string = null,
    		int? MinWidth_int = null,
    		Align Align = Align.Left,
    		bool Urgent = false,
    		bool Separator = true,
    		int? SeparatorBlockWidth=null,
    		Markup Markup = Markup.None,
    		Action<ClickEvent>? OnClick=null
    	);
    
    
    interface  ModuleParent
    {
    	public abstract void Schedule(int in_ms);
    	public abstract T GetGlobal<T>() where T: GlobalModuleResource, new();
    }
    
    abstract partial class StatusBar: ModuleParent
    {
    	static public string DefaultConfigText =
    @"
    refresh = 1
    
    [wireless]
    _type=i3status
    global_cache = 1
    name = wireless _first_
    config
    	format_up = ""W: (%quality %essid) %ip""
    	format_down = ""W: down""
    
    [ethernet]
    _type=i3status
    name=ethernet _first_
    config
         format_up = ""E: %ip (%speed)""
         format_down = ""E: down""
    
    [battery]
    
    [load]
    _type = i3status
    name = load
    config = format = ""%1min L""
    
    [memory]
    _type = i3status
    name = memory
    config
            format = ""%available""
            threshold_degraded = ""1G""
            format_degraded = ""MEMORY %available""
    
    [time]
    format = yyyy-MM-dd HH:mm:ss
    refresh
    ";
    	List<Module> modules = new();
    	protected void parseConfigFile(FileInfo? configFile)
    	{
    		if(configFile == null)
    		{
    			string configText;
    			string? fileName;
    			try
    			{
    				fileName = Environment.GetEnvironmentVariable("HOME")+"/.config/i3/i3csstatus.conf";
    				configText = File.ReadAllText(fileName);
    			}
    			catch(Exception e) when (e is FileNotFoundException || e is DirectoryNotFoundException)
    			{
    				fileName = null;
    				configText = DefaultConfigText;
    			}
    			parseConfig(new ConfigParser(configText, fileName));
    		}
    		else
    			parseConfigString(File.ReadAllText(configFile.ToString()), configFile.ToString());
    	}
    	protected void parseConfigString(string configString, string? fileName)
    	{
    		try
    		{
    			parseConfig(new ConfigParser(configString, fileName));
    		}
    		catch(ConfigException e) when (e.Parser.FileName == fileName && fileName != null)
    		{
    			throw new PrintAndExit(e.ErrorStringWithFile());
    		}
    	}
    	virtual protected void parseConfig(ConfigParser p)
    	{
    		var moduleTypes = new Dictionary<string, Type>(
    				from assembly in AppDomain.CurrentDomain.GetAssemblies()
    				from type in assembly.GetTypes()
    				where type.IsDefined(typeof(ModuleName), false)
    				where typeof(Module).IsAssignableFrom(type)
    				from name in type.GetCustomAttributes(typeof(ModuleName), false)
    				select new KeyValuePair<string, Type>(((ModuleName)name).GetName(), type)
    		);
    
    		var constructorSignature = new Type[]{};
    
    		foreach(var s in p)
    		{
    			ConfigValue type = s.OptionalDefaultIsSectionName("_type");
    			if(!moduleTypes.ContainsKey(type.AsString()))
    				throw new ConfigMistake(type, $"Module type with name \"{type.AsString()}\" not exists.");
    			var constructor = moduleTypes[type.AsString()].GetConstructor(constructorSignature);
    			if(constructor == null)
    				throw new Exception($"Missing constructor of {type} module");
    			Module module = (Module) constructor.Invoke(new object[]{});
    			modules.Add(addStandardModuleWrappers(s, module, this));
    			s.Use();
    			s.CheckUnused();
    		}
    		p.MainSection.CheckUnused();
    		p.CheckUnused();
    	}
    	public List<Block> Get()
    	{
    		var elements = new List<Block>();
    		foreach(var m in modules)
    			elements.AddRange(m.Get());
    		return elements;
    	}
    	public abstract void Schedule(int in_ms);
    	public abstract T GetGlobal<T>() where T: GlobalModuleResource, new();
    }
    
    
    class InnerStatusBar: StatusBar
    {
    	ModuleParent parrent;
    	public InnerStatusBar(ModuleParent _parrent, ConfigParser p)
    	{
    		parrent = _parrent;
    		parseConfig(p);
    	}
    	public override void Schedule(int in_ms) => parrent.Schedule(in_ms);
    	public override T GetGlobal<T>() => parrent.GetGlobal<T>();
    }
    
    
    abstract class RootStatusBar: StatusBar
    {
    	long nextRun = 0;
    	long refresh_ms;
    	object nextRunMonitor = new();
    	public RootStatusBar(FileInfo configFile)
    	{
    		parseConfigFile(configFile);
    	}
    	override protected void parseConfig(ConfigParser p)
    	{
    		refresh_ms = p.MainSection["refresh"]?.AsMs() ?? 1000;
    
    		base.parseConfig(p);
    
    		foreach(var g in globalModuleResources)
    			g.Value.InitEnd();
    	}
    	void wait()
    	{
    		lock(nextRunMonitor)
    		{
    			while(true)
    			{
    				long actual = Environment.TickCount64;
    				if(nextRun <= actual)
    					break;
    				long wait_ms = Math.Min(10000, nextRun - actual);
    				Monitor.Wait(nextRunMonitor, (int)wait_ms, true);
    			}
    			nextRun = Environment.TickCount64 + refresh_ms;
    		}
    	}
    	public void Run(TextWriter w)
    	{
    		initOutput(w);
    		while(true)
    		{
    			wait();
    			Print(w);
    		}
    	}
    	abstract protected void format(TextWriter w, List<Block> elements);
    	virtual protected void initOutput(TextWriter w){}
    	public void Print(TextWriter w)
    	{
    		foreach(var g in globalModuleResources)
    			g.Value.GetBegin();
    		var elements = Get();
    		foreach(var g in globalModuleResources)
    			g.Value.GetEnd();
    		format(w, elements);
    		w.Flush();
    	}
    	public override void Schedule(int in_ms)
    	{
    		lock(nextRunMonitor)
    		{
    			long actual = Environment.TickCount64;
    			if(nextRun <= actual + in_ms)
    				return;
    			nextRun = actual + in_ms;
    			Monitor.Pulse(nextRunMonitor);
    		}
    	}
    	Dictionary<Type, GlobalModuleResource> globalModuleResources = new();
    	public override T GetGlobal<T>()
    	{
    		if(!globalModuleResources.ContainsKey(typeof(T)))
    			globalModuleResources[typeof(T)] = new T();
    		return (T)globalModuleResources[typeof(T)];
    	}
    }
    
    
    class StatusBarPlainText: RootStatusBar
    {
    	public static string RemoveMarkup(string s, Markup m)
    	{
    		if(m == Markup.Pango)
    		{
    			bool inTag = new();
    			StringBuilder res = new();
    			foreach(char c in s)
    			{
    				if(c == '<') inTag = true;
    				if(!inTag) res.Append(c);
    				if(c == '>') inTag = false;
    			}
    			return res.ToString();
    		}
    		return s;
    	}
    	protected virtual string elementAsString(Block e)
    	{
    		string s = RemoveMarkup(e.Text, e.Markup);
    		if(e.MinWidth_string != null || e.MinWidth_int != null)
    		{
    			int minWidth = 0;
    			if(e.MinWidth_string != null)
    				minWidth = new StringInfo(e.MinWidth_string).LengthInTextElements;
    			if(e.MinWidth_int != null)
    				minWidth = (int)e.MinWidth_int/6;
    			int current = new StringInfo(s).LengthInTextElements;
    			if(current < minWidth)
    			{
    				if(e.Align == Align.Left) s = s + new String(' ', minWidth-current);
    				if(e.Align == Align.Right) s = new String(' ', minWidth-current) + s;
    				if(e.Align == Align.Center) s = new String(' ', (minWidth-current+1)/2) + s + new String(' ', (minWidth-current)/2);
    			}
    		}
    		return s;
    	}
    	public StatusBarPlainText(FileInfo configFile):base(configFile){}
    	override protected void format(TextWriter w, List<Block> elements)
    	{
    		Block? last = null;
    		foreach(var e in elements)
    		{
    			if(last != null)
    				w.Write(last.Separator?"|":" ");
    			w.Write(elementAsString(e));
    			last = e;
    		}
    		w.WriteLine("");
    	}
    }
    
    
    class StatusBarTerminal: StatusBarPlainText
    {
    	Thread? inputThread;
    	public StatusBarTerminal(FileInfo configFile, bool doInput):base(configFile)
    	{
    		if(doInput)
    		{
    			inputThread = new Thread(this.inputThreadFunc);
    			inputThread.IsBackground = true;
    			inputThread.Start();
    		}
    	}
    	void inputThreadFunc()
    	{
    		while(true)
    		{
    			System.ConsoleKeyInfo k = Console.ReadKey();
    			if(k.KeyChar == '\r')
    				Schedule(0);
    			else if(k.KeyChar == 'q')
    				Environment.Exit(0);
    			else
    				Console.WriteLine("");
    		}
    	}
    	override protected string elementAsString(Block e)
    	{
    		string msg = base.elementAsString(e);
    		if(e.Urgent) msg=$"\x1B[1m{msg}\x1B[0m";
    		if(e.Color != null) msg = msg.Pastel(e.Color.Value);
    		if(e.BackgroundColor != null) msg = msg.PastelBg(e.BackgroundColor.Value);
    		return msg;
    	}
    	override protected void format(TextWriter w, List<Block> elements)
    	{
    		Block? last = null;
    		foreach(var e in elements)
    		{
    			if(last != null)
    				w.Write((last.Separator?"|":" ").Pastel(Color.FromArgb(165, 229, 250)));
    			w.Write(elementAsString(e));
    			last = e;
    		}
    		w.WriteLine("");
    	}
    }
    class StatusBarI3: RootStatusBar
    {
    	Thread? inputThread;
    	record HistoryElement(
    			long time_ms,
    			long id,
    			IEnumerable<Block> data
    			);
    	Queue<HistoryElement> history = new(); // for finding correct lambda in inputThread
    	int outputCounter = 0;
    	public StatusBarI3(FileInfo configFile, bool doInput):base(configFile)
    	{
    		Console.Error.Flush();
    		if(doInput)
    		{
    			inputThread = new Thread(this.inputThreadFunc);
    			inputThread.IsBackground = true;
    			inputThread.Start();
    		}
    	}
    	void inputThreadFunc()
    	{
    		Console.ReadLine(); // read "["
    		while(true)
    		{
    
    			#pragma warning disable 8602
    			string? line = Console.ReadLine();
    
    			if(line == null) throw new Exception("I3bar close input");
    			if(line[0] == ',') line = line[1..];
    
    
    			JsonObject json = JsonObject.Parse(line).AsObject();
    			MouseButton button;
    			int jsonButton = json["button"].AsValue().GetValue<int>();
    			if(jsonButton == 1) button = MouseButton.Left; else
    			if(jsonButton == 2) button = MouseButton.Middle; else
    			if(jsonButton == 3) button = MouseButton.Right; else
    				continue;
    			Modifiers mod = 0;
    			foreach(var i in json["modifiers"].AsArray())
    			{
    				var s = i.AsValue().GetValue<string>();
    				if(s=="Shift") mod |= Modifiers.Shift;
    				if(s=="Control") mod |= Modifiers.Control;
    				if(s=="Mod4") mod |= Modifiers.Super;
    				if(s=="Mod1") mod |= Modifiers.Alt;
    			}
    			Console.Error.WriteLine(mod);
    			Console.Error.Flush();
    			var ev = new ClickEvent(
    					Relative: new Coordinates(json["relative_x"].AsValue().GetValue<int>(), json["relative_y"].AsValue().GetValue<int>()),
    					Absolute: new Coordinates(json["x"].AsValue().GetValue<int>(), json["y"].AsValue().GetValue<int>()),
    					Output: new Coordinates(json["output_x"].AsValue().GetValue<int>(), json["output_x"].AsValue().GetValue<int>()),
    					Size: new Coordinates(json["height"].AsValue().GetValue<int>(), json["width"].AsValue().GetValue<int>()),
    					Button: button,
    					Modifiers: mod
    					);
    			string name = json["name"].AsValue().GetValue<string>();
    			int outputId = int.Parse(name.Split(".")[0]);
    			int blockId = int.Parse(name.Split(".")[1]);
    			HistoryElement currentBar;
    			lock(history)
    			{
    				while(history.Count > 0 && history.Peek().id < outputId)
    					history.Dequeue();
    				if(history.Count <= 0) break;
    				currentBar = history.Peek();
    			}
    			var block = currentBar.data.ElementAt(blockId);
    			if(block.OnClick != null)
    				block.OnClick(ev);
    			#pragma warning restore 8602
    		}
    	}
    	override protected void initOutput(TextWriter w)
    	{
    		if(inputThread == null)
    			w.WriteLine("{\"version\":1}");
    		else
    			w.WriteLine("{\"version\":1, \"click_events\": true }");
    		w.WriteLine("[{}");
    	}
    	override protected void format(TextWriter w, List<Block> elements)
    	{
    		if(inputThread != null)
    		{
    			lock(history)
    			{
    				while(history.Count > 0 && history.Peek().time_ms < Environment.TickCount64 - 1000)
    					history.Dequeue();
    				history.Enqueue(new HistoryElement(Environment.TickCount64, outputCounter, elements));
    			}
    		}
    		JsonNode? intStringUnion(int? _int, string? _string)
    		{
    			JsonNode? n = null;
    			if(_int != null) n = _int;
    			if(_string != null) n = _string;
    			return n;
    		}
    		int i = 0;
    		var json = new JsonArray((from e in elements select new JsonObject(){
    			["name"] = $"{outputCounter}.{i++}",
    			["full_text"] = e.Text,
    			["short_text"] = e.ShortText,
    			["color"] = e.Color?.ToHex(),
    			["background"] = e.BackgroundColor?.ToHex(),
    			["border"] = e.BorderColor?.ToHex(),
    			["border_top"] = e.BorderTop,
    			["border_right"] = e.BorderTop,
    			["border_bottom"] = e.BorderTop,
    			["border_left"] = e.BorderTop,
    			["min_width"] = intStringUnion(e.MinWidth_int, e.MinWidth_string),
    			["align"] = e.Align==Align.Right?"right": e.Align==Align.Center?"center" :null,
    			["urgent"] = e.Urgent?true:null,
    			["separator"] = e.Separator?null:false,
    			["separator_block_width"] = e.SeparatorBlockWidth,
    			["markup"] = e.Markup==Markup.Pango?"pango": null
    		}.RemoveNull()).ToArray());
    		var opt = new JsonSerializerOptions();
    		opt.DefaultIgnoreCondition =  System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
    		w.Write(",");
    		w.WriteLine(json.ToJsonString(opt));
    		outputCounter++;
    	}
    }
    
    
    class Program
    {
    #pragma warning disable 1998
    	static void LinuxCatch(Action f)
    	{
    		try
    		{
    			f();
    		}
    		catch (PrintAndExit e)
    		{
    			Console.Error.WriteLine(e.ToPrint.Pastel(Color.FromArgb(255,0,0)));
    			Environment.Exit(e.ExitCode);
    		}
    		catch (Exception e)
    		{
    			Console.Error.WriteLine(e.ToString().Pastel(Color.FromArgb(255,0,0)));
    			Console.Error.WriteLine(
    @"
    
    this is not supposed to happen. Please report this bug to
    jirikalvoda+i3csstatus@kam.mff.cuni.cz. Thank you."
    					.Pastel(Color.FromArgb(255,0,0)));
    			Environment.Exit(134);
    		}
    	}
    	static int Main(string[] args)
    	{
    		var configOption = new Option<FileInfo>
    			("--config", "Path to configuration file. If not set configuration will be taken from ~/.config/i3/i3csstatus.conf or if there is no file, default config will be taken.");
    		configOption.AddAlias("-c");
    		var rootCommand = new RootCommand(
    @"i3csstatus - Alternative generator of i3 status bar written in c#
    
    (c)   2022 Jiri Kalvoda <jirikalvoda@kam.mff.cuni.cz>
    
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    any later version.
    
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.
    
    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.");
    		rootCommand.AddGlobalOption(configOption);
    
    		var c_plainText = new Command("plain-text", "Print status bar as plain text.");
    		rootCommand.Add(c_plainText);
    		c_plainText.SetHandler(async (config) =>
    				{
    					LinuxCatch(() => (new StatusBarPlainText(config)).Run(Console.Out));
    				}, configOption);
    
    		var c_terminal = new Command("terminal", "Print status bar with terminal escape secvence.");
    		rootCommand.Add(c_terminal);
    		var c_terminal_input = new Option<bool>
    			("--input", "Read key input.");
    		c_terminal_input.AddAlias("-i");
    		c_terminal.Add(c_terminal_input);
    		c_terminal.SetHandler(async (config, input) =>
    				{
    					LinuxCatch(() => (new StatusBarTerminal(config, input)).Run(Console.Out));
    				}, configOption, c_terminal_input);
    
    		var c_i3 = new Command("i3", "Comunicate with i3bar.");
    		rootCommand.Add(c_i3);
    		var c_i3_input = new Option<bool>
    			("--input", "Read mouse clicks.");
    		c_i3_input.AddAlias("-i");
    		c_i3.Add(c_i3_input);
    		c_i3.SetHandler(async (config, input) =>
    				{
    					LinuxCatch(() => (new StatusBarI3(config, input)).Run(Console.Out));
    				}, configOption, c_i3_input);
    
    		return rootCommand.Invoke(args);
    	}
    #pragma warning restore 1998
    }