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

Program.cs

Blame
  • Program.cs 11.33 KiB
    #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 Salaros.Configuration;
    
    using System.CommandLine;
    using System.Runtime.InteropServices;
    
    using Color = System.Drawing.Color;
    using Process = System.Diagnostics.Process;
    
    namespace i3csstatus {
    	static class ProcesExtended
    	{
    		[DllImport("libc", SetLastError = true)]
    		public static extern int kill(int pid, int sig);
    		public static int KillBySignal(this Process p, int sig)
    		{
    			return kill(p.Id, sig);
    		}
    	}
    	static class ColorToHexExtended
    	{
    		static public string ToHex(this Color c)
    		{
    			return $"#{c.R:X2}{c.G:X2}{c.B:X2}";
    		}
    	}
    	record Element(
    			string text,
    			string? short_text=null,
    			Color? color=null
    		);
    
    	[System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)]
    	public class ModuleName : System.Attribute
    	{
    		string name;
    		public ModuleName(string _name)
    		{
    			name = _name;
    		}
    		public string GetName() => name;
    	}
    	class ConfigException: Exception
    	{
    	}
    	class ConfigNotDefinedException: ConfigException
    	{
    		public string ModuleName;
    		public string AttributeName;
    		public ConfigNotDefinedException(string moduleName, string attributeName)
    		{
    			ModuleName = moduleName;
    			AttributeName = attributeName;
    		}
    		public ConfigNotDefinedException(ConfigSection section, string attributeName)
    			:this(section.SectionName, attributeName){}
    		public override string ToString()
    		{
    			return $"{ModuleName}: Attribute {AttributeName} is not defined.";
    		}
    	}
    	class ConfigParseAttributeException: ConfigException
    	{
    		string ModuleName, AttributeName;
    		string? Reason;
    		public ConfigParseAttributeException(string moduleName, string attributeName, string? reason=null)
    		{
    			ModuleName = moduleName;
    			AttributeName = attributeName;
    			Reason = reason;
    		}
    		public ConfigParseAttributeException(ConfigSection section, string attributeName, string? reason=null)
    			:this(section.SectionName, attributeName, reason){}
    		public override string ToString()
    		{
    			return $"{ModuleName}: Parsing attribute {AttributeName} fail" +
    				(Reason==null?".":$": {Reason}.");
    		}
    	}
    	static class ConfigExtended
    	{
    		static public string GetMandatory(this ConfigSection s, string attributeName)
    		{
    			string v = s[attributeName];
    			if(v == null)
    				throw new ConfigNotDefinedException(s.SectionName, attributeName);
    			return v;
    		}
    	}
    	class GlobalModuleResource
    	{
    		public virtual void InitEnd(){}
    		public virtual void GetBegin(){}
    		public virtual void GetEnd(){}
    	}
    	#nullable disable
    	interface Module
    	{
    		IEnumerable<Element> Get();
    		void Init(StatusBar _bar, ConfigParser config, ConfigSection section);
    	}
    	[ModuleName("constant")]
    	class ModuleConstant: Module
    	{
    		string text;
    		Color color;
    		public void Init(StatusBar _bar, ConfigParser config, ConfigSection section)
    		{
    			text = section.GetMandatory("text");
    			try
    			{
    				color = System.Drawing.ColorTranslator.FromHtml(section["color"] ?? "white");
    			} catch (ArgumentException)
    			{
    				throw new ConfigParseAttributeException(section, "color");
    			}
    		}
    		public IEnumerable<Element> Get()
    		{
    			return new Element[]{new Element(text, color: color)};
    		}
    	}
    	[ModuleName("i3status")]
    	class ModuleI3Status: Module
    	{
    		string name;
    		string configuration;
    		Element[] elements;
    		class Global: GlobalModuleResource
    		{
    			public List<ModuleI3Status> modules = new();
    			System.Diagnostics.Process process;
    			public override void InitEnd()
    			{
    				process = new();
    				//process.StartInfo.FileName = "tail";
    				//process.StartInfo.Arguments = "-n 5";
    				process.StartInfo.FileName = "i3status";
    				process.StartInfo.Arguments = "-c /dev/stdin";
    				process.StartInfo.RedirectStandardInput = true;
    				process.StartInfo.RedirectStandardOutput = true;
    				process.Start();
    				TextWriter stdin = process.StandardInput;
    				//stdin = Console.Out;
    				var stdout = process.StandardOutput;
    				stdin.WriteLine("general {");
    				stdin.WriteLine("output_format = \"i3bar\"");
    				stdin.WriteLine("colors = true");
    				stdin.WriteLine("interval = 1000000000");
    				stdin.WriteLine("}");
    				foreach(var m in modules)
    				{
    					stdin.WriteLine($"order += \"{m.name}\"");
    					stdin.WriteLine($"{m.name} {{");
    					if(m.configuration != null)
    						stdin.WriteLine(m.configuration);
    					stdin.WriteLine($"}}");
    				}
    				stdin.Close();
    				stdout.ReadLine();
    				stdout.ReadLine();
    				stdout.ReadLine();
    			}
    			static Element parseElement(JsonObject json)
    			{
    				return new Element(
    						text: json["full_text"].AsValue().GetValue<string>(),
    						short_text: json["short_text"]?.AsValue()?.GetValue<string>(),
    						color: System.Drawing.ColorTranslator.FromHtml(json["color"]?.AsValue()?.GetValue<string>() ?? "white")
    						);
    			}
    			public override void GetBegin()
    			{
    				process.KillBySignal(10);
    				string str = process.StandardOutput.ReadLine()[1..];
    				Console.WriteLine(str);
    				JsonArray json = JsonObject.Parse(str).AsArray();
    				if(json.Count != modules.Count)
    					throw new Exception("Parse i3status error");
    				for(int i=0;i<modules.Count;i++)
    				{
    					modules[i].elements = new []{parseElement(json[i].AsObject())};
    				}
    			}
    		}
    		public void Init(StatusBar _bar, ConfigParser config, ConfigSection section)
    		{
    			name = section.GetMandatory("name");
    			configuration = config.GetValue(section.SectionName, "config");
    			_bar.GetGlobal<Global>().modules.Add(this);
    		}
    		public IEnumerable<Element> Get() => elements ?? new Element[]{};
    	}
    	#nullable restore
    	abstract class StatusBar
    	{
    		List<Module> modules;
    		long nextRun = 0;
    		object nextRunMonitor = new();
    		public StatusBar(FileInfo configFile)
    		{
    			modules = new();
    			parseConfig(configFile);
    		}
    		void parseConfig(FileInfo configFile)
    		{
    			var p = new ConfigParser(configFile.ToString(),
    					new ConfigParserSettings
    					{
    					MultiLineValues = MultiLineValues.Simple | MultiLineValues.AllowValuelessKeys | MultiLineValues.QuoteDelimitedValues | MultiLineValues.AllowEmptyTopSection,
    					}
    				);
    
    			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.Sections)
    			{
    				string type = s["_type"] ?? s.SectionName;
    				var constructor = moduleTypes[type].GetConstructor(constructorSignature);
    				if(constructor == null)
    					throw new Exception($"Missing constructor of {type} module");
    				Module module = (Module) constructor.Invoke(new object[]{});
    				module.Init(this, p, s);
    				modules.Add(module);
    			}
    			foreach(var g in globalModuleResources)
    				g.Value.InitEnd();
    		}
    		abstract protected void format(TextWriter w, List<Element> elements);
    		virtual protected void initOutput(TextWriter w){}
    		public void Print(TextWriter w)
    		{
    			foreach(var g in globalModuleResources)
    				g.Value.GetBegin();
    			var elements = new List<Element>();
    			foreach(var m in modules)
    				elements.AddRange(m.Get());
    			foreach(var g in globalModuleResources)
    				g.Value.GetEnd();
    			format(w, elements);
    			w.Flush();
    		}
    		public void Schedule(int in_ms)
    		{
    			lock(nextRunMonitor)
    			{
    				long actual = Environment.TickCount64;
    				if(nextRun <= actual + in_ms)
    					return;
    				nextRun = actual + in_ms;
    				Monitor.Pulse(nextRunMonitor);
    			}
    		}
    		void wait()
    		{
    			lock(nextRunMonitor)
    			{
    				long actual = Environment.TickCount64;
    				if(nextRun <= actual)
    					return;
    				long wait_ms = Math.Min(10000, nextRun - actual);
    				Monitor.Wait(nextRunMonitor, (int)wait_ms, true);
    			}
    		}
    		public void Run(TextWriter w)
    		{
    			initOutput(w);
    			while(true)
    			{
    				wait();
    				nextRun = Environment.TickCount64 + 10000;
    				Print(w);
    			}
    		}
    		Dictionary<Type, GlobalModuleResource> globalModuleResources = new();
    		public T GetGlobal<T>() where T: GlobalModuleResource, new()
    		{
    			if(!globalModuleResources.ContainsKey(typeof(T)))
    				globalModuleResources[typeof(T)] = new T();
    			return (T)globalModuleResources[typeof(T)];
    		}
    	}
    	class StatusBarPlainText: StatusBar
    	{
    		public StatusBarPlainText(FileInfo configFile):base(configFile){}
    		override protected void format(TextWriter w, List<Element> elements)
    		{
    			bool first = true;
    			foreach(var e in elements)
    			{
    				if(!first)
    					w.Write("|");
    				first = false;
    				w.Write(e.text);
    			}
    			w.WriteLine("");
    		}
    	}
    	class StatusBarTerminal: StatusBar
    	{
    		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 void format(TextWriter w, List<Element> elements)
    		{
    			bool first = true;
    			foreach(var e in elements)
    			{
    				if(!first)
    					w.Write("|".Pastel(Color.FromArgb(165, 229, 250)));
    				first = false;
    				w.Write(e.color!=null?e.text.Pastel(e.color.Value):e.text);
    			}
    			w.WriteLine("");
    		}
    	}
    	class StatusBarI3: StatusBar
    	{
    		public StatusBarI3(FileInfo configFile):base(configFile){}
    		override protected void initOutput(TextWriter w)
    		{
    			w.WriteLine("{\"version\":1}");
    			w.WriteLine("[");
    		}
    		override protected void format(TextWriter w, List<Element> elements)
    		{
    			var json = new JsonArray((from e in elements select new JsonObject(){
    				["full_text"] = e.text,
    				["short_text"] = e.short_text,
    				["color"] = e.color?.ToHex(),
    			}).ToArray());
    			var opt = new JsonSerializerOptions();
    			opt.DefaultIgnoreCondition =  System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull;
    			w.WriteLine(json.ToJsonString(opt));
    		}
    	}
    	class Program
    	{
    	#pragma warning disable 1998
    		static int Main(string[] args)
    		{
    			var configOption = new Option<FileInfo>
    				("--config", "Path to configuration file.");
    			configOption.AddAlias("-c");
    			var rootCommand = new RootCommand("An alternative implementation for i3 status bar generating.");
    			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) =>
    					{
    						(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) =>
    					{
    						(new StatusBarTerminal(config, input)).Run(Console.Out);
    					}, configOption, c_terminal_input);
    
    			var c_i3 = new Command("i3", "Comunicate with i3bar.");
    			rootCommand.Add(c_i3);
    			c_i3.SetHandler(async (config) =>
    					{
    						(new StatusBarI3(config)).Run(Console.Out);
    					}, configOption);
    
    			rootCommand.Invoke(args);
    			return 0;
    		}
    	#pragma warning restore 1998
    	}
    }