Skip to content

Commit 78db2bc

Browse files
authored
Merge pull request #33 from DevMando/Publish/NugetPackage
Publish MandoCode as a NuGet CLI tool
2 parents ff4ba0d + 74ead8e commit 78db2bc

8 files changed

Lines changed: 143 additions & 37 deletions

File tree

src/MandoCode/Components/Banner.razor

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
PlayMatrixGlitch();
1010
}
1111

12-
var gradientText = BuildGradientBanner();
12+
var version = typeof(Program).Assembly.GetName().Version;
13+
var versionStr = version != null ? $"v{version.Major}.{version.Minor}.{version.Build}" : "";
14+
var gradientText = BuildGradientBanner(versionStr);
1315

1416
// Animation: (eyeColor, wavePhase)
1517
// wavePhase 0 = no howl waves, 1-3 = expanding waves
@@ -165,7 +167,7 @@
165167
);
166168
}
167169

168-
private static string BuildGradientBanner()
170+
private static string BuildGradientBanner(string version)
169171
{
170172
int maxWidth = _figletLines.Max(l => l.Length);
171173
var sb = new System.Text.StringBuilder();
@@ -201,6 +203,15 @@
201203
sb.Append('\n');
202204
}
203205

206+
// Version tag right-aligned under the figlet
207+
if (!string.IsNullOrEmpty(version))
208+
{
209+
var pad = Math.Max(0, maxWidth - version.Length);
210+
sb.Append('\n');
211+
sb.Append(new string(' ', pad));
212+
sb.Append($"[rgb(140,100,200)]{version}[/]");
213+
}
214+
204215
return sb.ToString();
205216
}
206217

src/MandoCode/MC.ico

64.5 KB
Binary file not shown.

src/MandoCode/MC.png

15.9 KB
Loading

src/MandoCode/MandoCode.csproj

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,23 @@
55
<TargetFramework>net8.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
8+
9+
<!-- dotnet tool settings -->
10+
<PackAsTool>true</PackAsTool>
11+
<ToolCommandName>mandocode</ToolCommandName>
12+
<PackageId>MandoCode</PackageId>
13+
14+
<!-- NuGet metadata -->
15+
<Version>0.9.0</Version>
16+
<Authors>Armando Fernandez (DevMando)</Authors>
17+
<Description>Your AI coding assistant — run locally or in the cloud with Ollama. No API keys required. Just you and your code.</Description>
18+
<PackageProjectUrl>https://github.com/DevMando/MandoCode</PackageProjectUrl>
19+
<RepositoryUrl>https://github.com/DevMando/MandoCode</RepositoryUrl>
20+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
21+
<PackageTags>ai;cli;coding-assistant;ollama;semantic-kernel;local-llm</PackageTags>
22+
<PackageReadmeFile>README.md</PackageReadmeFile>
23+
<PackageIcon>MC.png</PackageIcon>
24+
<ApplicationIcon>MC.ico</ApplicationIcon>
825
</PropertyGroup>
926

1027
<ItemGroup>
@@ -18,7 +35,12 @@
1835
</ItemGroup>
1936

2037
<ItemGroup>
21-
<Content Include="Audio\**\*.mp3" CopyToOutputDirectory="PreserveNewest" />
38+
<EmbeddedResource Include="Audio\**\*.mp3" />
39+
</ItemGroup>
40+
41+
<ItemGroup>
42+
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
43+
<None Include="MC.png" Pack="true" PackagePath="\" />
2244
</ItemGroup>
2345

2446
</Project>

src/MandoCode/Models/LearnContent.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ public static void Display()
2323
new Markup("Large language models with [bold]publicly available weights[/] that run on your own machine."),
2424
new Markup("Unlike cloud AI (ChatGPT, Claude), they are [green]free[/], [green]private[/], and [green]offline[/]."),
2525
new Markup(""),
26-
new Markup("Open-weight models give you full control — no API keys, no usage limits,"),
27-
new Markup("no data leaving your computer. Your code stays on your machine.")
26+
new Markup("Open-weight models give you full control — no API keys, no usage limits."),
27+
new Markup("Run locally or use Ollama cloud. Your code, your choice.")
2828
))
2929
{
3030
Border = BoxBorder.Rounded,

src/MandoCode/Models/MusicModels.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public record MusicTrackInfo
2525
public string Name { get; init; } = "";
2626
public string Genre { get; init; } = "";
2727
public string FileName { get; init; } = "";
28+
public string ResourceName { get; init; } = "";
2829
public string FilePath { get; init; } = "";
2930
public TimeSpan Duration { get; init; }
3031
}

src/MandoCode/Services/MusicPlayerService.cs

Lines changed: 102 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ namespace MandoCode.Services;
1010
public class MusicPlayerService : IDisposable
1111
{
1212
private readonly MandoCodeConfig _config;
13-
private readonly string _audioBasePath;
13+
private readonly System.Reflection.Assembly _assembly;
14+
private readonly string _userMusicPath;
1415
private readonly Random _random = new();
1516
private readonly object _lock = new();
1617

1718
private WaveOutEvent? _waveOut;
1819
private LoopStream? _loopStream;
19-
private AudioFileReader? _audioFileReader;
20+
private MemoryStream? _resourceStream;
21+
private Mp3FileReader? _mp3Reader;
22+
private WaveChannel32? _volumeChannel;
2023
private List<MusicTrackInfo> _tracks = new();
2124
private bool _disposed;
2225

@@ -28,41 +31,82 @@ public class MusicPlayerService : IDisposable
2831
public bool AudioAvailable { get; private set; } = true;
2932
public string? AudioError { get; private set; }
3033

34+
public string UserMusicPath => _userMusicPath;
35+
3136
public MusicPlayerService(MandoCodeConfig config)
3237
{
3338
_config = config;
39+
_assembly = typeof(MusicPlayerService).Assembly;
40+
_userMusicPath = Path.Combine(
41+
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
42+
".mandocode", "music");
3443

35-
// Resolve audio path relative to the application binary
36-
var appDir = AppContext.BaseDirectory;
37-
_audioBasePath = Path.Combine(appDir, "Audio");
38-
44+
EnsureUserMusicFolders();
3945
DiscoverTracks();
4046
}
4147

4248
/// <summary>
43-
/// Discovers all MP3 files in Audio/lofi/ and Audio/synthwave/ directories.
49+
/// Creates ~/.mandocode/music/lofi/ and ~/.mandocode/music/synthwave/ if they don't exist.
50+
/// </summary>
51+
private void EnsureUserMusicFolders()
52+
{
53+
try
54+
{
55+
Directory.CreateDirectory(Path.Combine(_userMusicPath, "lofi"));
56+
Directory.CreateDirectory(Path.Combine(_userMusicPath, "synthwave"));
57+
}
58+
catch { /* non-critical */ }
59+
}
60+
61+
/// <summary>
62+
/// Discovers MP3 tracks from embedded resources and the user's ~/.mandocode/music/ folder.
63+
/// Embedded resource names follow: {RootNamespace}.Audio.{genre}.{filename}.mp3
64+
/// User tracks follow: ~/.mandocode/music/{genre}/{filename}.mp3
4465
/// </summary>
4566
private void DiscoverTracks()
4667
{
4768
_tracks.Clear();
4869

49-
if (!Directory.Exists(_audioBasePath))
50-
return;
51-
52-
foreach (var genreDir in Directory.GetDirectories(_audioBasePath))
70+
// 1. Embedded resources (bundled defaults)
71+
var prefix = "MandoCode.Audio.";
72+
foreach (var resourceName in _assembly.GetManifestResourceNames())
5373
{
54-
var genre = Path.GetFileName(genreDir).ToLowerInvariant();
55-
var mp3Files = Directory.GetFiles(genreDir, "*.mp3", SearchOption.TopDirectoryOnly);
74+
if (!resourceName.StartsWith(prefix) || !resourceName.EndsWith(".mp3"))
75+
continue;
76+
77+
var afterPrefix = resourceName[prefix.Length..];
78+
var firstDot = afterPrefix.IndexOf('.');
79+
if (firstDot < 0) continue;
5680

57-
foreach (var mp3 in mp3Files)
81+
var genre = afterPrefix[..firstDot];
82+
var trackFile = afterPrefix[(firstDot + 1)..];
83+
var trackName = Path.GetFileNameWithoutExtension(trackFile).Replace('_', ' ');
84+
85+
_tracks.Add(new MusicTrackInfo
86+
{
87+
Name = trackName,
88+
Genre = genre,
89+
FileName = trackFile,
90+
ResourceName = resourceName
91+
});
92+
}
93+
94+
// 2. User's custom tracks from ~/.mandocode/music/{genre}/*.mp3
95+
if (Directory.Exists(_userMusicPath))
96+
{
97+
foreach (var genreDir in Directory.GetDirectories(_userMusicPath))
5898
{
59-
_tracks.Add(new MusicTrackInfo
99+
var genre = Path.GetFileName(genreDir).ToLowerInvariant();
100+
foreach (var mp3 in Directory.GetFiles(genreDir, "*.mp3", SearchOption.TopDirectoryOnly))
60101
{
61-
Name = Path.GetFileNameWithoutExtension(mp3),
62-
Genre = genre,
63-
FileName = Path.GetFileName(mp3),
64-
FilePath = mp3
65-
});
102+
_tracks.Add(new MusicTrackInfo
103+
{
104+
Name = Path.GetFileNameWithoutExtension(mp3),
105+
Genre = genre,
106+
FileName = Path.GetFileName(mp3),
107+
FilePath = mp3
108+
});
109+
}
66110
}
67111
}
68112
}
@@ -105,7 +149,7 @@ public bool Play(string? genre = null)
105149
genreTracks = _tracks.ToList();
106150
if (genreTracks.Count == 0)
107151
{
108-
AudioError = "No MP3 files found. Add .mp3 files to Audio/lofi/ or Audio/synthwave/ directories.";
152+
AudioError = $"No MP3 files found. Drop .mp3 files into ~/.mandocode/music/{{genre}}/ (e.g. {_userMusicPath}/lofi/)";
109153
return false;
110154
}
111155
}
@@ -114,7 +158,7 @@ public bool Play(string? genre = null)
114158
MusicTrackInfo track;
115159
if (genreTracks.Count > 1 && CurrentTrack != null)
116160
{
117-
var candidates = genreTracks.Where(t => t.FilePath != CurrentTrack.FilePath).ToList();
161+
var candidates = genreTracks.Where(t => t != CurrentTrack).ToList();
118162
track = candidates[_random.Next(candidates.Count)];
119163
}
120164
else
@@ -126,7 +170,7 @@ public bool Play(string? genre = null)
126170
}
127171

128172
/// <summary>
129-
/// Plays a specific track with looped playback.
173+
/// Plays a specific track with looped playback. Supports both embedded resources and local files.
130174
/// </summary>
131175
private bool PlayTrack(MusicTrackInfo track)
132176
{
@@ -137,9 +181,33 @@ private bool PlayTrack(MusicTrackInfo track)
137181

138182
try
139183
{
140-
_audioFileReader = new AudioFileReader(track.FilePath);
141-
_audioFileReader.Volume = _config.Music.Volume;
142-
_loopStream = new LoopStream(_audioFileReader);
184+
if (!string.IsNullOrEmpty(track.FilePath))
185+
{
186+
// Local file track
187+
_mp3Reader = new Mp3FileReader(track.FilePath);
188+
}
189+
else
190+
{
191+
// Embedded resource track
192+
var stream = _assembly.GetManifestResourceStream(track.ResourceName);
193+
if (stream == null)
194+
{
195+
AudioError = $"Embedded audio resource not found: {track.ResourceName}";
196+
return false;
197+
}
198+
199+
// Copy to MemoryStream for seeking support (required for looping)
200+
_resourceStream = new MemoryStream();
201+
stream.CopyTo(_resourceStream);
202+
_resourceStream.Position = 0;
203+
stream.Dispose();
204+
205+
_mp3Reader = new Mp3FileReader(_resourceStream);
206+
}
207+
208+
_volumeChannel = new WaveChannel32(_mp3Reader);
209+
_volumeChannel.Volume = _config.Music.Volume;
210+
_loopStream = new LoopStream(_volumeChannel);
143211

144212
_waveOut = new WaveOutEvent();
145213
_waveOut.Init(_loopStream);
@@ -189,14 +257,18 @@ private void StopInternal()
189257
_waveOut?.Stop();
190258
_waveOut?.Dispose();
191259
_loopStream?.Dispose();
192-
_audioFileReader?.Dispose();
260+
_volumeChannel?.Dispose();
261+
_mp3Reader?.Dispose();
262+
_resourceStream?.Dispose();
193263
}
194264
catch { /* Swallow disposal errors */ }
195265
finally
196266
{
197267
_waveOut = null;
198268
_loopStream = null;
199-
_audioFileReader = null;
269+
_volumeChannel = null;
270+
_mp3Reader = null;
271+
_resourceStream = null;
200272
}
201273
}
202274

@@ -242,9 +314,9 @@ public void SetVolume(float volume)
242314

243315
lock (_lock)
244316
{
245-
if (_audioFileReader != null)
317+
if (_volumeChannel != null)
246318
{
247-
_audioFileReader.Volume = volume;
319+
_volumeChannel.Volume = volume;
248320
}
249321
}
250322

src/MandoCode/Services/MusicPlayerUI.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public static void RenderTrackList(MusicPlayerService player)
9191
if (tracks.Count == 0)
9292
{
9393
Console.WriteLine($" {LabelClr}No tracks found.{Rst}");
94-
Console.WriteLine($" {DimClr}Add .mp3 files to Audio/lofi/ or Audio/synthwave/ directories.{Rst}");
94+
Console.WriteLine($" {DimClr}Drop .mp3 files into ~/.mandocode/music/lofi/ or ~/.mandocode/music/synthwave/{Rst}");
9595
Console.WriteLine();
9696
return;
9797
}
@@ -107,7 +107,7 @@ public static void RenderTrackList(MusicPlayerService player)
107107

108108
foreach (var track in group)
109109
{
110-
var playing = player.CurrentTrack?.FilePath == track.FilePath;
110+
var playing = player.CurrentTrack == track;
111111
var marker = playing ? $"{StateClr}\u25b6" : " ";
112112
Console.WriteLine($" {Border}\u2502 {marker} {TrackClr}\u266b {track.Name}{Rst}");
113113
}

0 commit comments

Comments
 (0)