Skip to content

Commit 0cfeffb

Browse files
author
RECEP UNCU
committed
Updated .gitignore to include .NET Core, VS, and NuGet rules.
Added NodeVersionSwitcher project and solution files. Included nodejs-mini.png and nodejs.ico as content files. Implemented NodeVersionSwitcherContext for tray menu. Added Program class with Main method as entry point.
1 parent 73955f2 commit 0cfeffb

7 files changed

Lines changed: 404 additions & 0 deletions

.gitignore

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# .NET Core
2+
# Tüm bin ve obj klasörlerini dışarıda bırak
3+
bin/
4+
obj/
5+
6+
# Kullanıcıya özgü dosyalar
7+
*.user
8+
*.userosscache
9+
*.suo
10+
*.sln.docstates
11+
12+
# .NET Core SDK ve araçları tarafından oluşturulan dosyalar
13+
project.lock.json
14+
project.fragment.lock.json
15+
artifacts/
16+
17+
# Visual Studio için kullanıcıya özgü dosyalar
18+
.vs/
19+
.vscode/
20+
21+
# .NET Core çalışma zamanını dahil etme
22+
dotnet-compile*
23+
dotnet-cache
24+
25+
# NuGet Paketler
26+
*.nupkg
27+
28+
# Log dosyaları
29+
*.log
30+
31+
# Yedekleme dosyaları
32+
*~

NodeVersionSwitcher.csproj

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>WinExe</OutputType>
5+
<TargetFramework>net9.0-windows</TargetFramework>
6+
<Nullable>enable</Nullable>
7+
<UseWindowsForms>true</UseWindowsForms>
8+
<ImplicitUsings>enable</ImplicitUsings>
9+
<Title>Node Version Switcher</Title>
10+
<Description>A Windows system tray application that helps you quickly switch between different Node.js versions installed via NVM (Node Version Manager) for Windows. The app provides easy access to your installed Node versions through a context menu in the system tray, allowing seamless version switching with a single click.</Description>
11+
<Copyright>Copyright © 2025 Recep UNCU. All rights reserved.</Copyright>
12+
<PackageIcon>nodejs-mini.png</PackageIcon>
13+
<StartupObject>NodeVersionSwitcher.Program</StartupObject>
14+
<ApplicationIcon>nodejs.ico</ApplicationIcon>
15+
</PropertyGroup>
16+
17+
<ItemGroup>
18+
<None Remove="nodejs-mini.png" />
19+
<None Remove="nodejs.ico" />
20+
</ItemGroup>
21+
22+
<ItemGroup>
23+
<Content Include="nodejs-mini.png">
24+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
25+
</Content>
26+
<Content Include="nodejs.ico">
27+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
28+
</Content>
29+
</ItemGroup>
30+
31+
</Project>

NodeVersionSwitcher.sln

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.12.35527.113
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NodeVersionSwitcher", "NodeVersionSwitcher.csproj", "{74547383-2CF4-40EC-B47E-53AA996F017B}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{74547383-2CF4-40EC-B47E-53AA996F017B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{74547383-2CF4-40EC-B47E-53AA996F017B}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{74547383-2CF4-40EC-B47E-53AA996F017B}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{74547383-2CF4-40EC-B47E-53AA996F017B}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
EndGlobal

NodeVersionSwitcherContext.cs

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
using Microsoft.Win32;
2+
using System.Diagnostics;
3+
4+
namespace NodeVersionSwitcher;
5+
6+
/// <summary>
7+
/// Provides a context menu for switching between Node.js versions using NVM for Windows.
8+
/// </summary>
9+
internal class NodeVersionSwitcherContext : ApplicationContext
10+
{
11+
private readonly string _nvmPath;
12+
private readonly NotifyIcon _trayIcon;
13+
14+
/// <summary>
15+
/// Initializes a new instance of the <see cref="NodeVersionSwitcherContext"/> class.
16+
/// </summary>
17+
public NodeVersionSwitcherContext()
18+
{
19+
_nvmPath = GetNvmInstallationPath();
20+
21+
_trayIcon = new NotifyIcon
22+
{
23+
Visible = true,
24+
Text = "Node Version Switcher",
25+
Icon = LoadTrayIcon(),
26+
ContextMenuStrip = CreateContextMenu()
27+
};
28+
}
29+
30+
/// <summary>
31+
/// Determines whether the application is set to start with Windows.
32+
/// </summary>
33+
/// <returns></returns>
34+
private bool IsInStartup()
35+
{
36+
var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", false);
37+
if (key == null) return false;
38+
39+
var value = key.GetValue("NodeVersionSwitcher") as string;
40+
41+
return value == Application.ExecutablePath;
42+
}
43+
44+
/// <summary>
45+
/// Adds the application to the Windows startup folder.
46+
/// </summary>
47+
private void AddToStartup()
48+
{
49+
var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
50+
key?.SetValue("NodeVersionSwitcher", Application.ExecutablePath);
51+
}
52+
53+
/// <summary>
54+
/// Removes the application from the Windows startup folder.
55+
/// </summary>
56+
private void RemoveFromStartup()
57+
{
58+
var key = Registry.CurrentUser.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", true);
59+
key?.DeleteValue("NodeVersionSwitcher", false);
60+
}
61+
62+
/// <summary>
63+
/// Loads the tray icon from the application directory.
64+
/// </summary>
65+
/// <returns></returns>
66+
private Icon LoadTrayIcon()
67+
{
68+
var iconPath = Path.Combine(Application.StartupPath, "nodejs.ico");
69+
return File.Exists(iconPath) ? new Icon(iconPath) : SystemIcons.Application;
70+
}
71+
72+
/// <summary>
73+
/// Creates the context menu for the tray icon.
74+
/// </summary>
75+
/// <returns></returns>
76+
private ContextMenuStrip CreateContextMenu()
77+
{
78+
var contextMenu = new ContextMenuStrip();
79+
PopulateNodeVersionsMenu(contextMenu);
80+
81+
contextMenu.Opening += (s, e) =>
82+
{
83+
contextMenu.Items.Clear();
84+
contextMenu.Items.Add("Node Version Switcher");
85+
contextMenu.Items.Add(new ToolStripSeparator());
86+
PopulateNodeVersionsMenu(contextMenu);
87+
AddStartWithWindowsMenuItem(contextMenu);
88+
AddExitMenuItem(contextMenu);
89+
contextMenu.Refresh();
90+
};
91+
92+
return contextMenu;
93+
}
94+
95+
/// <summary>
96+
/// Populates the context menu with the available Node.js versions.
97+
/// </summary>
98+
/// <param name="contextMenu"></param>
99+
private void PopulateNodeVersionsMenu(ContextMenuStrip contextMenu)
100+
{
101+
var nodeVersions = GetNodeVersions().ToList();
102+
103+
if (!nodeVersions.Any())
104+
{
105+
contextMenu.Items.Add(new ToolStripMenuItem("No versions found") { Enabled = false });
106+
return;
107+
}
108+
109+
var currentVersion = GetCurrentVersion();
110+
111+
foreach (var version in nodeVersions)
112+
{
113+
var menuItem = new ToolStripMenuItem($"{version}{(version == currentVersion ? " (Current)" : string.Empty)}");
114+
menuItem.Click += (sender, args) => SwitchNodeVersion(version);
115+
contextMenu.Items.Add(menuItem);
116+
}
117+
118+
contextMenu.Items.Add(new ToolStripSeparator());
119+
}
120+
121+
/// <summary>
122+
/// Switches the Node.js version to the specified version.
123+
/// </summary>
124+
/// <param name="version"></param>
125+
private void SwitchNodeVersion(string version)
126+
{
127+
try
128+
{
129+
UseNodeVersion(version);
130+
MessageBox.Show($"Switched to Node.js {version}", "NVM Tray", MessageBoxButtons.OK, MessageBoxIcon.Information);
131+
}
132+
catch (Exception ex)
133+
{
134+
MessageBox.Show($"Failed to switch Node.js version: {ex.Message}", "NVM Tray", MessageBoxButtons.OK, MessageBoxIcon.Error);
135+
}
136+
}
137+
138+
/// <summary>
139+
/// Adds an "Exit" menu item to the context menu.
140+
/// </summary>
141+
/// <param name="contextMenu"></param>
142+
private void AddExitMenuItem(ContextMenuStrip contextMenu)
143+
{
144+
var exitMenuItem = new ToolStripMenuItem("Exit");
145+
exitMenuItem.Click += (s, e) =>
146+
{
147+
_trayIcon.Visible = false;
148+
Application.Exit();
149+
};
150+
contextMenu.Items.Add(exitMenuItem);
151+
}
152+
153+
/// <summary>
154+
/// Adds a "Start with Windows" menu item to the context menu.
155+
/// </summary>
156+
/// <param name="contextMenu"></param>
157+
private void AddStartWithWindowsMenuItem(ContextMenuStrip contextMenu)
158+
{
159+
var startWithWindowsMenuItem = new ToolStripMenuItem("Start with Windows")
160+
{
161+
Checked = IsInStartup()
162+
};
163+
164+
startWithWindowsMenuItem.Click += (s, e) =>
165+
{
166+
if (startWithWindowsMenuItem.Checked)
167+
{
168+
RemoveFromStartup();
169+
startWithWindowsMenuItem.Checked = false;
170+
}
171+
else
172+
{
173+
AddToStartup();
174+
startWithWindowsMenuItem.Checked = true;
175+
}
176+
};
177+
178+
contextMenu.Items.Add(startWithWindowsMenuItem);
179+
contextMenu.Items.Add(new ToolStripSeparator());
180+
}
181+
182+
/// <summary>
183+
/// Gets the available Node.js versions.
184+
/// </summary>
185+
/// <returns></returns>
186+
private IEnumerable<string> GetNodeVersions()
187+
{
188+
return Directory.GetDirectories(_nvmPath, "v*").Select(Path.GetFileName);
189+
}
190+
191+
/// <summary>
192+
/// Gets the current Node.js version.
193+
/// </summary>
194+
/// <returns></returns>
195+
private string GetCurrentVersion()
196+
{
197+
try
198+
{
199+
var filePath = Path.Combine(GetLinkPath(), "node.exe");
200+
var versionInfo = FileVersionInfo.GetVersionInfo(filePath);
201+
return !string.IsNullOrEmpty(versionInfo.FileVersion) ? $"v{versionInfo.FileVersion}" : string.Empty;
202+
}
203+
catch (Exception ex)
204+
{
205+
Debug.WriteLine($"Error retrieving current version: {ex.Message}");
206+
return string.Empty;
207+
}
208+
}
209+
210+
/// <summary>
211+
/// Switches the Node.js version to the specified version.
212+
/// </summary>
213+
/// <param name="version"></param>
214+
private void UseNodeVersion(string version)
215+
{
216+
var targetPath = Path.Combine(_nvmPath, version);
217+
var linkPath = GetLinkPath();
218+
219+
if (Directory.Exists(linkPath))
220+
{
221+
Directory.Delete(linkPath, true);
222+
}
223+
224+
Directory.CreateSymbolicLink(linkPath, targetPath);
225+
}
226+
227+
/// <summary>
228+
/// Gets the installation path of NVM for Windows.
229+
/// </summary>
230+
/// <returns></returns>
231+
/// <exception cref="DirectoryNotFoundException"></exception>
232+
private string GetNvmInstallationPath()
233+
{
234+
var nvmHome = Environment.GetEnvironmentVariable("NVM_HOME") ?? string.Empty;
235+
if (Directory.Exists(nvmHome)) return nvmHome;
236+
237+
var registryPaths = new[]
238+
{
239+
@"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\nvm",
240+
@"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\nvm"
241+
};
242+
243+
foreach (var path in registryPaths)
244+
{
245+
var installLocation = GetRegistryValue(Registry.LocalMachine, path, "InstallLocation");
246+
if (Directory.Exists(installLocation)) return installLocation;
247+
}
248+
249+
var defaultPaths = new[]
250+
{
251+
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), "nvm"),
252+
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData\\Roaming\\nvm")
253+
};
254+
255+
return defaultPaths.FirstOrDefault(Directory.Exists) ??
256+
throw new DirectoryNotFoundException("NVM installation directory not found");
257+
}
258+
259+
/// <summary>
260+
/// Gets the path of the symbolic link used by NVM.
261+
/// </summary>
262+
/// <returns></returns>
263+
/// <exception cref="Exception"></exception>
264+
private string GetLinkPath()
265+
{
266+
var settingsFilePath = Path.Combine(_nvmPath, "settings.txt");
267+
268+
try
269+
{
270+
var line = File.ReadLines(settingsFilePath).FirstOrDefault(l => l.StartsWith("path:"));
271+
return line?.Substring("path:".Length).Trim() ?? string.Empty;
272+
}
273+
catch (Exception ex)
274+
{
275+
throw new Exception($"Error reading link path: {ex.Message}");
276+
}
277+
}
278+
279+
/// <summary>
280+
/// Gets a registry value from the specified key and subkey path.
281+
/// </summary>
282+
/// <param name="baseKey"></param>
283+
/// <param name="subKeyPath"></param>
284+
/// <param name="valueName"></param>
285+
/// <returns></returns>
286+
private static string GetRegistryValue(RegistryKey baseKey, string subKeyPath, string valueName)
287+
{
288+
using var key = baseKey.OpenSubKey(subKeyPath);
289+
return key?.GetValue(valueName) as string;
290+
}
291+
292+
/// <summary>
293+
/// Disposes of the resources used by the application context.
294+
/// </summary>
295+
/// <param name="disposing"></param>
296+
protected override void Dispose(bool disposing)
297+
{
298+
if (disposing)
299+
{
300+
_trayIcon.Dispose();
301+
}
302+
base.Dispose(disposing);
303+
}
304+
}

0 commit comments

Comments
 (0)