diff --git a/src/main.zig b/src/main.zig index 1f4644e..6bba538 100644 --- a/src/main.zig +++ b/src/main.zig @@ -7,7 +7,7 @@ const DeviceAction = enum { toggle, }; -fn sendWemoCommand(url_base: []const u8, action: DeviceAction, allocator: std.mem.Allocator) !void { +fn sendWemoCommand(device_entry: std.hash_map.StringHashMap([]const u8).Entry, action: DeviceAction, allocator: std.mem.Allocator) !void { const state = switch (action) { .on => "1", .off => "0", @@ -27,13 +27,17 @@ fn sendWemoCommand(url_base: []const u8, action: DeviceAction, allocator: std.me , .{state}); defer allocator.free(soap_body); - const url = try std.fmt.allocPrint(allocator, "{s}/upnp/control/basicevent1", .{url_base}); + const url = try std.fmt.allocPrint(allocator, "{s}/upnp/control/basicevent1", .{device_entry.value_ptr.*}); defer allocator.free(url); var client = std.http.Client{ .allocator = allocator }; defer client.deinit(); - std.log.debug("sending wemo command to {s}, action={s}", .{ url_base, @tagName(action) }); + std.log.debug("sending WeMo {s} command to '{s}' at {s}\n", .{ + @tagName(action), + device_entry.key_ptr.*, + device_entry.value_ptr.*, + }); const res = try client.fetch(.{ .method = .POST, .payload = soap_body, @@ -47,21 +51,32 @@ fn sendWemoCommand(url_base: []const u8, action: DeviceAction, allocator: std.me if (res.status == .ok) { var stdout_writer = std.fs.File.stdout().writer(&.{}); const stdout = &stdout_writer.interface; - try stdout.print("WeMo command sent: url={s}, Action={s}\n", .{ url_base, @tagName(action) }); + try stdout.print("WeMo {s} command sent to '{s}' at {s}\n", .{ + @tagName(action), + device_entry.key_ptr.*, + device_entry.value_ptr.*, + }); } else { var stderr_writer = std.fs.File.stderr().writer(&.{}); const stderr = &stderr_writer.interface; - try stderr.print("WeMo command sent: url={s}, Action={s}\n", .{ url_base, @tagName(action) }); + try stderr.print("ERROR: Could not send WeMo {s} command to '{s}' at {s}\n", .{ + @tagName(action), + device_entry.key_ptr.*, + device_entry.value_ptr.*, + }); } } -fn loadDeviceConfig(allocator: std.mem.Allocator) !std.StringHashMap([]const u8) { +fn loadDeviceConfig(allocator: std.mem.Allocator, bin_dir: []const u8) !std.StringHashMap([]const u8) { var devices = std.StringHashMap([]const u8).init(allocator); var stderr_writer = std.fs.File.stderr().writer(&.{}); const stderr = &stderr_writer.interface; // Try controlData.json first - if (std.fs.cwd().openFile("controlData.json", .{})) |file| { + const json_path = try std.fs.path.join(allocator, &[_][]const u8{ bin_dir, "controlData.json" }); + defer allocator.free(json_path); + + if (std.fs.openFileAbsolute(json_path, .{})) |file| { defer file.close(); const content = try file.readToEndAlloc(allocator, 1024 * 1024); @@ -72,7 +87,7 @@ fn loadDeviceConfig(allocator: std.mem.Allocator) !std.StringHashMap([]const u8) "Failed to parse controlData.json: {}. Ignoring controlData.json, looking for devices.txt", .{err}, ); - return loadDevicesFromTxt(allocator); + return loadDevicesFromTxt(allocator, bin_dir); }; defer parsed.deinit(); @@ -94,14 +109,17 @@ fn loadDeviceConfig(allocator: std.mem.Allocator) !std.StringHashMap([]const u8) return devices; } else |_| { - return loadDevicesFromTxt(allocator); + return loadDevicesFromTxt(allocator, bin_dir); } } -fn loadDevicesFromTxt(allocator: std.mem.Allocator) !std.StringHashMap([]const u8) { +fn loadDevicesFromTxt(allocator: std.mem.Allocator, bin_dir: []const u8) !std.StringHashMap([]const u8) { var devices = std.StringHashMap([]const u8).init(allocator); - const file = std.fs.cwd().openFile("devices.txt", .{}) catch |err| switch (err) { + const txt_path = try std.fs.path.join(allocator, &[_][]const u8{ bin_dir, "devices.txt" }); + defer allocator.free(txt_path); + + const file = std.fs.openFileAbsolute(txt_path, .{}) catch |err| switch (err) { error.FileNotFound => { var stderr_writer = std.fs.File.stderr().writer(&.{}); const stderr = &stderr_writer.interface; @@ -148,7 +166,7 @@ fn parseAction(action_words: [][]const u8) ?DeviceAction { return null; } -fn extractDevice(allocator: std.mem.Allocator, object_words: [][]const u8, devices: *std.StringHashMap([]const u8)) !?[]const u8 { +fn extractDevice(allocator: std.mem.Allocator, object_words: [][]const u8, devices: *std.StringHashMap([]const u8)) !?std.hash_map.StringHashMap([]const u8).Entry { if (object_words.len == 0) return null; var total_size: usize = 0; @@ -157,14 +175,29 @@ fn extractDevice(allocator: std.mem.Allocator, object_words: [][]const u8, devic var allocating_writer = try std.Io.Writer.Allocating.initCapacity(allocator, total_size); defer allocating_writer.deinit(); const writer = &allocating_writer.writer; - for (object_words, 1..) |word, i| { - var buf: [256]u8 = undefined; - const lower_string = std.ascii.lowerString(&buf, word); - try writer.print("{s}{s}", .{ lower_string, if (i < object_words.len) " " else "" }); - } + for (0..object_words.len) |removed_words| { + defer allocating_writer.clearRetainingCapacity(); + const total_words = object_words.len - removed_words; + for (object_words, 1..) |word, i| { + if (i >= total_words + 1) break; + var buf: [256]u8 = undefined; + const lower_string = std.ascii.lowerString(&buf, word); + try writer.print("{s}{s}", .{ + lower_string, + if (i < object_words.len - removed_words) " " else "", + }); + } - if (devices.get(allocating_writer.written())) |url_base| return url_base; - std.log.warn("Could not find device '{s}'", .{allocating_writer.written()}); + std.log.debug( + "Attempting device match '{s}'", + .{allocating_writer.written()}, + ); + if (devices.getEntry(allocating_writer.written())) |entry| return entry; + std.log.info( + "Could not find device match '{s}'. Falling back to more general object", + .{allocating_writer.written()}, + ); + } return null; } @@ -187,7 +220,16 @@ pub fn main() !void { } std.log.debug("loading device config", .{}); - var devices = loadDeviceConfig(gpa.allocator()) catch |err| { + // Get binary directory + const bin_path = std.fs.selfExePathAlloc(gpa.allocator()) catch |err| { + stderr.print("Failed to get binary path: {}\n", .{err}) catch {}; + std.process.exit(1); + }; + defer gpa.allocator().free(bin_path); + + const bin_dir = std.fs.path.dirname(bin_path) orelse "."; + + var devices = loadDeviceConfig(gpa.allocator(), bin_dir) catch |err| { stderr.print("Failed to load device configuration: {}\n", .{err}) catch {}; std.process.exit(1); }; @@ -201,7 +243,7 @@ pub fn main() !void { } std.log.debug("initializing parser", .{}); - var parser = pos.Parser.init(gpa.allocator()) catch |err| { + var parser = pos.Parser.initWithDataDir(gpa.allocator(), bin_dir) catch |err| { stderr.print("Failed to initialize parser: {}\n", .{err}) catch {}; std.process.exit(1); }; @@ -232,8 +274,8 @@ pub fn main() !void { std.debug.print("\n", .{}); if (parseAction(action_words)) |action| { - if (try extractDevice(allocator, object_words, &devices)) |url_base| { - try sendWemoCommand(url_base, action, gpa.allocator()); + if (try extractDevice(allocator, object_words, &devices)) |entry| { + try sendWemoCommand(entry, action, gpa.allocator()); } else { try stdout.print("Device not found in configuration\n", .{}); } diff --git a/src/root.zig b/src/root.zig index 7d26b9d..f8745b7 100644 --- a/src/root.zig +++ b/src/root.zig @@ -115,11 +115,54 @@ pub const Parser = struct { const opts = c.parse_options_create(); if (opts == null) return error.ParseOptionsCreationFailed; + setOptions(opts); + + return Parser{ + .dict = dict, + .opts = opts, + .allocator = allocator, + }; + } + + fn setOptions(opts: anytype) void { c.parse_options_set_verbosity(opts, 0); c.parse_options_set_linkage_limit(opts, 100); c.parse_options_set_disjunct_cost(opts, 2); c.parse_options_set_min_null_count(opts, 0); c.parse_options_set_max_null_count(opts, 0); + } + + pub fn initWithDataDir(allocator: std.mem.Allocator, data_dir: []const u8) !Parser { + const dict_path = try std.fs.path.join(allocator, &[_][]const u8{ data_dir, "data/4.0.dict" }); + defer allocator.free(dict_path); + const knowledge_path = try std.fs.path.join(allocator, &[_][]const u8{ data_dir, "data/4.0.knowledge" }); + defer allocator.free(knowledge_path); + const constituent_path = try std.fs.path.join(allocator, &[_][]const u8{ data_dir, "data/4.0.constituent-knowledge" }); + defer allocator.free(constituent_path); + const affix_path = try std.fs.path.join(allocator, &[_][]const u8{ data_dir, "data/4.0.affix" }); + defer allocator.free(affix_path); + + const dict_cstr = try allocator.dupeZ(u8, dict_path); + defer allocator.free(dict_cstr); + const knowledge_cstr = try allocator.dupeZ(u8, knowledge_path); + defer allocator.free(knowledge_cstr); + const constituent_cstr = try allocator.dupeZ(u8, constituent_path); + defer allocator.free(constituent_cstr); + const affix_cstr = try allocator.dupeZ(u8, affix_path); + defer allocator.free(affix_cstr); + + const dict = c.dictionary_create( + @ptrCast(@constCast(dict_cstr.ptr)), + @ptrCast(@constCast(knowledge_cstr.ptr)), + @ptrCast(@constCast(constituent_cstr.ptr)), + @ptrCast(@constCast(affix_cstr.ptr)), + ); + if (dict == null) return error.DictionaryCreationFailed; + + const opts = c.parse_options_create(); + if (opts == null) return error.ParseOptionsCreationFailed; + + setOptions(opts); return Parser{ .dict = dict,